Solidity v0.8.5 allows conversions from bytes to bytesNN values, adds the verbatim builtin function to inject arbitrary bytecode in Yul and fixes several smaller bugs.

Notable New Features

Bytes Conversion

Find the complete feature documentation here.

This release introduces the ability to convert bytes and bytes slices to fixed bytes types bytes1 / … / bytes32. While conversion between fixed-length bytes types has always been possible, it is now also possible to convert dynamically-sized bytes types to fixed-length bytes types.

In case a byte array is longer than the target fixed bytes type, it will be truncated at the end:

function f(bytes memory c) public pure returns (bytes8) {
	// If c is longer than 8 bytes, truncation happens
	return bytes8(c); 
}

Calling f("12345678") in Solidity code will return "12345678", as will calling it as f("1234567890"). If the array is shorter than the target fixed type, it will be padded with zeros at the end, so calling f("1234") will return "1234".

A nice example of using the bytes conversion feature would be its application in proxies:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.5;
contract Proxy {
	/// @dev Address of the client contract managed by this proxy
	address client;
	constructor(address _client) {
		client = _client;
	}
	/// Forwards all calls to the client but performs additional checks for calls to "setOwner(address)".
	function forward(bytes calldata _payload) external {
		require(_payload.length >= 4);
		bytes4 sig = bytes4(_payload[:4]);
		if (sig == bytes4(keccak256("setOwner(address)"))) {
			address owner = abi.decode(_payload[4:], (address));
			require(owner != address(0), "Address of owner cannot be zero.");
		}
		(bool status,) = client.delegatecall(_payload);
		require(status, "Forwarded call failed.");
	}
}

Before, it was not possible to do bytes4 sig = bytes4(_payload[:4]);, instead you had to use the following:

bytes4 sig =
	_payload[0] |
	(bytes4(_payload[1]) >> 8) |
	(bytes4(_payload[2]) >> 16) |
	(bytes4(_payload[3]) >> 24);

Verbatim in Yul

Find the complete feature documentation here.

This release introduces the set of verbatim builtin functions for Yul that allows you inject arbitrary bytecode into the binary. This is currently only available via pure Yul, i.e., it is not accessible via inline assembly.

This essentially has two use-cases (more on those below):

  1. The use of opcodes unknown to Yul (because they are only proposed or because you are targeting an EVM-incompatible chain).
  2. Generation of specific sequences of bytecode that are unmodified by the optimizer.

The functions are verbatim<n>i_<m>o("<data>", ...), where

  • n is a decimal between 0 and 99 that specifies the number of input stack slots / variables,
  • m is a decimal between 0 and 99 that specifies the number of output stack slots / variables,
  • data is a string literal that contains the sequence of bytes.

Note that there are some caveats when it comes to using verbatim. Details about it can be found in the documentation.

Using new Opcodes

As a practical example, one can use this to conveniently inject a newly proposed EVM opcode into the binary. Take the proposed BASEFEE (at 0x48) opcode (see EIP-3198 and EIP-1559), since the Solidity compiler currently does not support this opcode, one can use verbatim to implement it in Yul.

{
	function basefee() -> out {
		out := verbatim_0i_1o(hex"48")
	}

	sstore(0, basefee())
}

Here’s another example that has an input parameter for verbatim.

let x := calldataload(0)
// The hex"600202" corresponds to EVM instructions:
// PUSH 02 MUL
// That is, it multiplies x by 2.
let double := verbatim_1i_1o(hex"600202", x)

The above code will result in a dup1 opcode to retrieve x (the optimizer may directly use the result of the calldataload opcode, though) directly followed by 600202. The code is assumed to consume the (copied) value of x and produce the result on the top of the stack. The compiler then generates code to allocate a stack slot for double and store the result there.

Use-Case for Optimism

The second use-case can be useful for Layer-2-solutions like Optimism, but also other situations like bytecode analysis or debugging come to mind. Optimism currently uses a custom Solidity compiler because they simulate the execution of a smart contract where every change to the state (storage, external calls, etc.) is not executed directly, but it is replaced by a call to a manager contract that stores the change for verification. The problem with this is checking whether or not a contract conforms to these restrictions (i.e. properly calls the manager contract for each change), especially since this has to be done by the on-chain fraud detection mechanism. What they do is that they check that none of the state-changing opcodes is used by the contract, with the exception of the call opcode that calls the manager contract. For this exception to be properly detected, the sequence of opcodes that leads to this call opcode has to have a specific form and usually, the Solidity optimizer does some rearranging and destroys this form. Luckily, verbatim can solve this problem such that Optimism does not need to rely on a custom Solidity compiler anymore and can use all later versions of the Solidity compiler without modifications.

The optimism compiler could take the Yul code generated by the Solidity compiler, append the following Yul helper functions and syntactically replace all state-changing builtin function calls with their ovm_-counterparts. For example, all sstore(x, y) calls are relpaced by ovm_sstore(x, y) calls. After this replacement, the Yul optimizer can even be run again. (This code only illustrates sstore.)

/// Generic call to the manager contract.
function ovm_callManager(arguments, arguments_size, output_area, output_area_size) {
	verbatim_4i_0o(
		hex"336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b",
		arguments,
		arguments_size,
		output_area,
		output_area_size
	)
}

// Call a manager function with two arguments
function ovm_kall_2i(signature, x, y) {
	// Store touched memory in locals and restore it at the end.
	let tmp_a := mload(0x00)
	let tmp_b := mload(0x20)
	let tmp_c := mload(0x40)
	mstore(0, signature)
	mstore(4, x)
	mstore(0x24, y)
	ovm_callManager(0, 0x44, 0, 0)
	mstore(0x00, tmp_a)
	mstore(0x20, tmp_b)
	mstore(0x40, tmp_c)
}

// Replace all calls to ``sstore(x, y)`` by ``ovm_sstore(x, y)``
function ovm_sstore(x, y) {
	// The hex code is the selector of
	// the sstore function on the manager contract.
	ovm_kall_2i(hex"22bd64c0", x, y)
}

Full Changelog

Language Features:

  • Allowing conversion from bytes and bytes slices to bytes1/…/bytes32.
  • Yul: Add verbatim builtin function to inject arbitrary bytecode.

Compiler Features:

  • Code Generator: Insert helper functions for panic codes instead of inlining unconditionally. This can reduce costs if many panics (checks) are inserted, but can increase costs where few panics are used.
  • EVM: Set the default EVM version to “Berlin”.
  • SMTChecker: Function definitions can be annotated with the custom Natspec tag custom:smtchecker abstract-function-nondet to be abstracted by a nondeterministic value when called.
  • Standard JSON / combined JSON: New artifact “functionDebugData” that contains bytecode offsets of entry points of functions and potentially more information in the future.
  • Yul Optimizer: Evaluate keccak256(a, c), when the value at memory location a is known at compile time and c is a constant <= 32.

Bugfixes:

  • AST: Do not output value of Yul literal if it is not a valid UTF-8 string.
  • Code Generator: Fix internal error when function arrays are assigned to storage variables and the function types can be implicitly converted but are not identical.
  • Code Generator: Fix internal error when super would have to skip an unimplemented function in the virtual resolution order.
  • Control Flow Graph: Assume unimplemented modifiers use a placeholder.
  • Control Flow Graph: Take internal calls to functions that always revert into account for reporting unused or unassigned variables.
  • Function Call Graph: Fix internal error connected with circular constant references.
  • Name Resolver: Do not issue shadowing warning if the shadowing name is not directly accessible.
  • Natspec: Allow multiple @return tags on public state variable documentation.
  • SMTChecker: Fix internal error on conversion from bytes to fixed bytes.
  • SMTChecker: Fix internal error on external calls from the constructor.
  • SMTChecker: Fix internal error on struct constructor with fixed bytes member initialized with string literal.
  • Source Locations: Properly set source location of scoped blocks.
  • Standard JSON: Properly allow the inliner setting under settings.optimizer.details.
  • Type Checker: Fix internal compiler error related to having mapping types in constructor parameter for abstract contracts.
  • Type Checker: Fix internal compiler error when attempting to use an invalid external function type on pre-byzantium EVMs.
  • Type Checker: Fix internal compiler error when overriding receive ether function with one having different parameters during inheritance.
  • Type Checker: Make errors about (nested) mapping type in event or error parameter into fatal type errors.

AST Changes:

  • Add member hexValue for Yul string and hex literals.

A big thank you to all contributors who helped make this release possible!

Download the new version of Solidity here.