Summary
EscrowFactory.computeEscrowAddress() computes the contract address. Refactoring the computation process using Assembly results in considerable gas savings.
Vulnerability Details
The function EscrowFactory.computeAddress() can be refactored to calculate addresses using Assembly to save gas on deployment and upon each call.
Impact
Gas
Tools Used
Forge, Foundry Toolkit (gas report, gas snapshots)
Recommendation
Refactor address computation using Yul. Consider the refactored EscrowFactory contract below:
pragma solidity 0.8.18;
import {IEscrowFactory} from "./IEscrowFactory.sol";
import {IEscrow} from "./IEscrow.sol";
import {Escrow} from "./Escrow.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
contract EscrowFactory is IEscrowFactory {
using SafeERC20 for IERC20;
function newEscrow(
uint256 price,
IERC20 tokenContract,
address seller,
address arbiter,
uint256 arbiterFee,
bytes32 salt
) external returns (IEscrow) {
bytes32 bytecodeHash = keccak256(abi.encodePacked(type(Escrow).creationCode, abi.encode(price, tokenContract, msg.sender, seller, arbiter, arbiterFee)));
address computedAddress = computeAddress(salt, bytecodeHash, address(this));
tokenContract.safeTransferFrom(msg.sender, computedAddress, price);
Escrow escrow = new Escrow{salt: salt}(
price,
tokenContract,
msg.sender,
seller,
arbiter,
arbiterFee
);
if (address(escrow) != computedAddress) {
revert EscrowFactory__AddressesDiffer();
}
emit EscrowCreated(address(escrow), msg.sender, seller, arbiter);
return escrow;
}
* @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at
* `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
*/
function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) public pure returns (address addr) {
assembly {
let ptr := mload(0x40)
mstore(add(ptr, 0x40), bytecodeHash)
mstore(add(ptr, 0x20), salt)
mstore(ptr, deployer)
let start := add(ptr, 0x0b)
mstore8(start, 0xff)
addr := keccak256(start, 85)
}
}
}
With this change in place, considerable gas savings were observed.
An overall gas saving of 23261 gas was seen across all tests. The tests testComputedAddressEqualsDeployedAddress() and testCreatingEscrowEmitsEvent() saw the highest gas savings of 8377 gas and 9344 gas respectively. All other tests saw a saving of around 200 gas.
The contract size reduced by 249 bytes, from 8622 to 8373. The deployment cost went from 1720616 to1670760, seeing a gas saving of 49856 gas.
Note that the tests have to be modified slightly to account for this change. The changes shown in the modified test below should be replicated in all tests where Escrow is instantiated:
function testCreatingEscrowEmitsEvent() public hasTokensApprovedForSending {
bytes32 bytecodeHash = keccak256(abi.encodePacked(type(Escrow).creationCode, abi.encode(PRICE, i_tokenContract, BUYER, SELLER, ARBITER, ARBITER_FEE)));
address computedAddress = escrowFactory.computeAddress(SALT1, bytecodeHash, address(escrowFactory));
vm.prank(BUYER);
vm.expectEmit(true, true, true, true, address(escrowFactory));
emit EscrowCreated(computedAddress, BUYER, SELLER, ARBITER);
escrowFactory.newEscrow(PRICE, i_tokenContract, SELLER, ARBITER, ARBITER_FEE, SALT1);
}