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);
}