Gas Optimizations for the Rest of Us

区块链安全 2年前 (2022) admin
763 0 0

 

Gas Optimizations for the Rest of Us

 

The basics of optimizing Solidity contracts, explained for regular coders.


Writing smart contracts is hard. Not only do you get a single chance to write bug-free code, but depending on exactly how you write, it’ll cost your users more or less to interact with it.

When you compile a smart contract, every line of Solidity gets converted into a series of operations (called opcodes), which have a set gas cost. Your goal is to write your program using as little opcodes as possible (or replace the most expensive with cheaper ones).

Of course, this is all very complex, so let’s take it slowly. Instead of going down the opcode rabbit hole, here are some simple optimizations you can apply to your contracts today.

Bump your Solidity version

The Solidity version your contracts’ use is defined at the top of the file, like so:

pragma solidity ^0.8.0;

In this context, means that the contract will use the newest version of Solidity available from the series .^0.8.00.8.x

Newer versions of Solidity sometimes include gas optimizations along with bug fixes and security patches, so updating to the latest version will not only make your code safer but (often) cheaper as well.

To catch most of the recent optimizations, make sure you’re using at least , like so:0.8.4

pragma solidity ^0.8.4;

Drop Counters.sol

If you’re using any of the OpenZeppelin contracts for your NFT project or token, it’s likely you’re using OZ’s library.Counters.sol

In newer versions of Solidity ( onwards), this library isn’t super useful, and replacing it with a regular integer can save some gas. Here’s how:0.8

contract TestCounters {
-	using Counters for Counters.Counter;
-	Counters.Counter private _tokenIds;
	
+	uint256 private _tokenId;
	
	function mint() public {
-		_tokenIds.increment();
-		uint256 tokenId = _tokenIds.current();
		
+		uint256 tokenId = _tokenId++;
	}
}

Mark immutable variables

Wether it’s the amount of decimals for a token, USDC’s address, or a payout account, sometimes there are contract variables we don’t ever plan on changing. Marking these as either constants (if you write them in the code) or immutable (if you plan on giving them a value later, e.g. via the constructor) can reduce the cost of accessing those values. Here’s an example:

contract TestImmutable {
	uint256 internal constant DECIMALS = 18;
	address public immutable currencyToken;
	
	constructor(address _currencyToken) {
		currencyToken = _currencyToken;
	}
}

unchecked {}

Starting with Solidity , all math operations include checks for overflows. This is great (and replaces the SafeMath library, so you can drop that if you’re using it!), but it costs extra gas, so we want to avoid it when not necessary.0.8

Overflow checks basically make sense that you don’t subtract from zero, or add to 2^256 (the maximum number Solidity can handle). So, for example, if you’re just incrementing a token id or storing an ERC20 value, you should opt out of these checks using :unchecked {}

contract TestUnchecked is ERC721 {
	ERC20 internal immutable paymentToken = ERC20(address(0x1));
	uint256 internal _tokenId;
	
	mapping(address => uint256) _balances;
	
	function mint(uint256 amount) public {
		_mint(msg.sender, _tokenId);
		
		unchecked {
			_balances[msg.sender] += amount;
			++tokenId;
		}
		
		paymentToken.transferFrom(msg.sender, address(this), amount);
	}
}

This comes in especially handy with for loops, where the value you increment will never realistically overflow, so you save gas on every iteration:i

contract TestUncheckedFor {
	ERC20 internal immutable token = ERC20(address(0x1));

	function refundAddresses(address[] calldata accounts) {
		// ? pro tip: save the array length to a variable instead of
		// inlining to save gas on every iteration.
		uint256 len = accounts.length;
		
		for (uint256 i = 0; i < len; ) {
			token.transfer(accounts[i], 1 ether);
			
			unchecked { ++i; }
		}
	}
}

Avoid copying arguments to memory

For some types of arguments, like strings or arrays, solidity forces you to specify a location for storing them (either or ). Using here is much cheaper, so you’re gonna want to use that as much as possible, leaving only for when you intend to modify the arguments (since specifying makes them read-only).memorycalldatacalldatamemorycalldata

Use custom errors

Solidity introduced a new feature, allowing developers to specify custom errors, which are defined and behave similar to events:0.8.4

contract TestErrors {
	// first, define the error
	error Unauthorized();
	
	// errors can have parameters, like events
	error AlreadyMinted(uint256 id);
	
	// ? pro tip: this gets set to the deployer address
	// sometimes, you don't need Ownable :)
	address internal immutable owner = msg.sender;
	
	mapping(uint256 => address) _ownerOf;
	
	function ownerMint(uint256 tokenId) public {
		if (msg.sender != owner) revert Unauthorized();
		if (_ownerOf[tokenId] != address(0)) revert AlreadyMinted(tokenId);
		
		_ownerOf[tokenId] = msg.sender;
	}
}

You should try to use these custom errors instead of the old revert strings (), since those could cost extra gas depending on the message.require(true, "error message here")

Honorable mentions

When using any kind of counters (like ), starting it off at instead of will make the first mint slightly cheaper. In general, writing a value to a slot that doesn’t have one will be more expensive than writing to one that does, so keep that in mind._tokenId10

Also, when incrementing an integer, (return old value, then add 1) is cheaper than (add 1, then return new value). If you’re just incrementing a counter and ignoring the return value, you probably want the first one.++ii++

When dividing, Solidity inserts a check to make sure we’re not dividing by zero. If you know for sure the divisor isn’t zero, you can perform the operation using assembly and save some extra gas, like so:

contract TestDivision {
	function divide_by_2(uint256 a) public pure returns (uint256 result) {
		assembly {
			result := div(a, 2)
		}
	}
}

Finally, functions marked as will be cheaper to call than non-payable functions. Keep in mind marking everything as payable might impact user experience, since they’ll get an extra field when using Etherscan, and might accidentally send some ETH to the contract when you don’t expect them to. A relatively safe optimization is to mark the constructor as , slightly reducing deployment cost.payablepayable

Closing

While hard at times, the world of Solidity and the EVM is really interesting. Some devs can spend days and days making slight tweaks to their code, trying to shove a few extra gas units off their contracts.

For everyone else though, I hope the above list can serve as a good resource for making your contracts a bit cheaper ?

 

原文始发于Miguel Piedrafita:Gas Optimizations for the Rest of Us

版权声明:admin 发表于 2022年3月17日 下午8:43。
转载请注明:Gas Optimizations for the Rest of Us | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...