40,000 USDC
View results
Submission Details
Severity: gas
Valid

Perform Escrow address computation using assembly

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:

// SPDX-License-Identifier: MIT
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";
/// @author Cyfrin
/// @title EscrowFactory
/// @notice Factory contract for deploying Escrow contracts.
contract EscrowFactory is IEscrowFactory {
using SafeERC20 for IERC20;
/// @inheritdoc IEscrowFactory
/// @dev msg.sender must approve the token contract to spend the price amount before calling this function.
/// @dev There is a risk that if a malicious token is used, the dispute process could be manipulated.
/// Therefore, careful consideration should be taken when chosing the token.
function newEscrow(
uint256 price,
IERC20 tokenContract,
address seller,
address arbiter,
uint256 arbiterFee,
bytes32 salt
) external returns (IEscrow) {
// Compute
bytes32 bytecodeHash = keccak256(abi.encodePacked(type(Escrow).creationCode, abi.encode(price, tokenContract, msg.sender, seller, arbiter, arbiterFee)));
address computedAddress = computeAddress(salt, bytecodeHash, address(this));
// Transfer funds
tokenContract.safeTransferFrom(msg.sender, computedAddress, price);
Escrow escrow = new Escrow{salt: salt}(
price,
tokenContract,
msg.sender,
seller,
arbiter,
arbiterFee
);
// Ensure address equality
if (address(escrow) != computedAddress) {
revert EscrowFactory__AddressesDiffer();
}
// Return escrow instance
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) {
/// @solidity memory-safe-assembly
assembly {
// Load free mem ptr on stack
let ptr := mload(0x40)
// Store bytecodeHash at 0x00 (ptr) + 0x40, so starting at 64thnd byte
mstore(add(ptr, 0x40), bytecodeHash)
// Store salt at ptr + 0x20, so starting at 32nd byte
mstore(add(ptr, 0x20), salt)
// Store deployer addr starting at 0x00. Since it 20 bytes and rightalined, 12 garbage bytes on the left
mstore(ptr, deployer)
// We want to hash addr+salt+byteCodeHash, so move the starting ptr to the 11th byte
let start := add(ptr, 0x0b)
// Set garbage byte right before the first addr byte (the 12th byte from the start) to 0xff
// This is to avoid clash with CREATE
//from 0x0b(11th byte), store 0xff, which gets stored at 12th byte
mstore8(start, 0xff)
// Hash from the 12th byte until the 85th byte (1byte0xff+20byteAddr+32byteSalt+32byteBytecodeHash)
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);
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.