Skip to content

Commit

Permalink
optimize encodeTransactionData by implementing it in assembly
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 committed Oct 18, 2024
1 parent a0a1d42 commit be08482
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 45 deletions.
95 changes: 66 additions & 29 deletions contracts/Safe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -387,18 +387,30 @@ contract Safe is
}

/**
* @notice Returns the pre-image of the transaction hash (see getTransactionHash).
* @param to Destination address.
* @param value Ether value.
* @param data Data payload.
* @param operation Operation type.
* @param safeTxGas Gas that should be used for the safe transaction.
* @param baseGas Gas costs for that are independent of the transaction execution(e.g. base transaction fee, signature check, payment of the refund)
* @param gasPrice Maximum gas price that should be used for this transaction.
* @param gasToken Token address (or 0 if ETH) that is used for the payment.
* @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
* @param _nonce Transaction nonce.
* @return Transaction hash bytes.
* @notice Encodes transaction data according to EIP-712 typed structured data hashing scheme.
* @dev This function creates a hash of the transaction data using a specific structure and combines it
* with the domain separator to create an EIP-712 compliant message. The function uses assembly
* for gas optimization in memory allocation and struct hashing.
*
* @param to The target address where the transaction will be executed.
* @param value The amount of Ether (in wei) to be sent with the transaction.
* @param data The calldata to be executed at the target address.
* @param operation The type of operation to execute (e.g., Call or DelegateCall).
* @param safeTxGas Gas limit explicitly set for the safe transaction execution.
* @param baseGas Gas costs that are independent of the transaction execution
* (e.g., base transaction fee, signature check, refund payment).
* @param gasPrice Maximum gas price (in wei) to be used for this transaction.
* Used for refund calculation.
* @param gasToken Address of the token used for gas payment refunds.
* Use address(0) for ETH.
* @param refundReceiver Address that should receive the gas payment refund.
* Use address(0) to refund tx.origin.
* @param _nonce Sequential number used to prevent transaction reuse.
*
* @return result The encoded transaction data in the format:
* [0x19, 0x01, domainSeparator, safeTxStructHash].
* Total length is 66 bytes: 2 bytes for prefix + 32 bytes for domain separator
* + 32 bytes for transaction struct hash.
*/
function encodeTransactionData(
address to,
Expand All @@ -411,23 +423,48 @@ contract Safe is
address gasToken,
address refundReceiver,
uint256 _nonce
) private view returns (bytes memory) {
bytes32 safeTxStructHash = keccak256(
abi.encode(
SAFE_TX_TYPEHASH,
to,
value,
keccak256(data),
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
_nonce
)
);
return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxStructHash);
) private view returns (bytes memory result) {
bytes32 separatorHash = domainSeparator();

// The encoding is written in assembly to avoid memory re-allocations.
// Straightforward solidity implementation would allocate multiple times, but since the only reason it allocates is because it needs to hash the data,
// we can save gas by allocating the memory in a single operation and then operating in this block of memory.
/* solhint-disable no-inline-assembly */
assembly {
let freeMemoryPointer := mload(0x40)

// Copy the data from calldata to memory
calldatacopy(freeMemoryPointer, data.offset, data.length)

let dataEnd := add(freeMemoryPointer, data.length)
// Prepare the SafeTX struct for hashing
mstore(dataEnd, SAFE_TX_TYPEHASH)
mstore(add(dataEnd, 32), to)
mstore(add(dataEnd, 64), value)
mstore(add(dataEnd, 96), keccak256(freeMemoryPointer, data.length))
mstore(add(dataEnd, 128), operation)
mstore(add(dataEnd, 160), safeTxGas)
mstore(add(dataEnd, 192), baseGas)
mstore(add(dataEnd, 224), gasPrice)
mstore(add(dataEnd, 256), gasToken)
mstore(add(dataEnd, 288), refundReceiver)
mstore(add(dataEnd, 320), _nonce)

// Hash the SafeTX struct and store it at the end of the result
// Hashing first so we can re-use the same memory block for the result
mstore(add(dataEnd, 66), keccak256(dataEnd, 352))
// Store the domain separator
mstore(add(dataEnd, 34), separatorHash)
// Store the EIP-712 prefix (0x1901)
mstore8(add(dataEnd, 33), 0x01)
mstore8(add(dataEnd, 32), 0x19)
// Store the length of the encoded data (always 66 bytes)
mstore(dataEnd, 66)

// Return the encoded transaction data (including the length)
result := dataEnd
}
/* solhint-enable no-inline-assembly */
}

/**
Expand Down
40 changes: 24 additions & 16 deletions test/core/Safe.Signatures.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getCompatFallbackHandler } from "./../utils/setup";
import { calculateSafeMessageHash, signHash, buildContractSignature } from "./../../src/utils/execution";
import { expect } from "chai";
import hre from "hardhat";
import crypto from "crypto";
import { AddressZero } from "@ethersproject/constants";
import { getSafeTemplate, getSafe } from "../utils/setup";
import {
Expand Down Expand Up @@ -46,22 +47,29 @@ describe("Safe", () => {
it("should correctly calculate EIP-712 hash", async () => {
const { safe } = await setupTests();
const safeAddress = await safe.getAddress();
const tx = buildSafeTransaction({ to: safeAddress, nonce: await safe.nonce() });
const typedDataHash = calculateSafeTransactionHash(safeAddress, tx, await chainId());
await expect(
await safe.getTransactionHash(
tx.to,
tx.value,
tx.data,
tx.operation,
tx.safeTxGas,
tx.baseGas,
tx.gasPrice,
tx.gasToken,
tx.refundReceiver,
tx.nonce,
),
).to.be.eq(typedDataHash);

for (let i = 0; i < 100; i++) {
const randomAddress = "0x" + crypto.randomBytes(20).toString("hex");
const randomValue = "0x" + crypto.randomBytes(32).toString("hex");
const randomData = "0x" + crypto.randomBytes(128).toString("hex");

const tx = buildSafeTransaction({ to: randomAddress, nonce: await safe.nonce(), value: randomValue, data: randomData });
const typedDataHash = calculateSafeTransactionHash(safeAddress, tx, await chainId());
await expect(
await safe.getTransactionHash(
tx.to,
tx.value,
tx.data,
tx.operation,
tx.safeTxGas,
tx.baseGas,
tx.gasPrice,
tx.gasToken,
tx.refundReceiver,
tx.nonce,
),
).to.be.eq(typedDataHash);
}
});
});

Expand Down

0 comments on commit be08482

Please sign in to comment.