Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 102 additions & 232 deletions transpiler/CHANGELOG.md

Large diffs are not rendered by default.

1,742 changes: 1,347 additions & 395 deletions transpiler/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion transpiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@types/node": "^25.0.9",
"tsx": "^4.0.0",
"typescript": "^5.3.0",
"viem": "^2.0.0"
"viem": "^2.0.0",
"vitest": "^4.0.18"
}
}
26 changes: 25 additions & 1 deletion transpiler/runtime-replacements.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
{ "name": "completeOwnershipHandover", "visibility": "public", "params": ["pendingOwner: string"], "returns": "void" },
{ "name": "owner", "visibility": "public", "returns": "string" },
{ "name": "ownershipHandoverExpiresAt", "visibility": "public", "params": ["pendingOwner: string"], "returns": "bigint" }
]
],
"mixin": " // Ownable mixin (from secondary base class)\n private _owner: string = '0x0000000000000000000000000000000000000000';\n protected _initializeOwner(newOwner: string): void {\n this._owner = newOwner;\n }\n protected _checkOwner(): void {\n if (this._msg.sender !== this._owner) {\n throw new Error(\"Unauthorized\");\n }\n }\n owner(): string {\n return this._owner;\n }"
}
},
{
Expand Down Expand Up @@ -73,6 +74,29 @@
{ "name": "canonicalHash", "visibility": "internal", "params": ["signature: string"], "returns": "string", "static": true }
]
}
},
{
"source": "lib/EIP712.sol",
"reason": "Complex Yul assembly for gas-optimized EIP-712 domain separator and typed data hashing",
"runtimeModule": "../runtime",
"exports": ["EIP712"],
"interface": {
"class": "EIP712",
"extends": "Contract",
"abstract": true,
"constants": [
{ "name": "_DOMAIN_TYPEHASH", "type": "string", "visibility": "internal" }
],
"methods": [
{ "name": "_domainNameAndVersion", "visibility": "protected", "returns": "[string, string]", "abstract": true },
{ "name": "_domainNameAndVersionMayChange", "visibility": "protected", "returns": "boolean" },
{ "name": "_domainSeparator", "visibility": "protected", "returns": "string" },
{ "name": "_hashTypedData", "visibility": "protected", "params": ["structHash: string"], "returns": "string" },
{ "name": "eip712Domain", "visibility": "public", "returns": "object" },
{ "name": "hashTypedData", "visibility": "public", "params": ["structHash: string"], "returns": "string" }
],
"mixin": " // EIP712 mixin (from secondary base class)\n static readonly _DOMAIN_TYPEHASH: string = '0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f';\n protected _domainNameAndVersion(): [string, string] {\n return ['', ''];\n }\n protected _domainSeparator(): `0x${string}` {\n const [name, version] = this._domainNameAndVersion();\n return keccak256(encodeAbiParameters(\n [{type: 'bytes32'}, {type: 'bytes32'}, {type: 'bytes32'}, {type: 'uint256'}, {type: 'address'}],\n [EIP712._DOMAIN_TYPEHASH as `0x${string}`, keccak256(encodePacked(['string'], [name])), keccak256(encodePacked(['string'], [version])), 31337n, this._contractAddress as `0x${string}`]\n ));\n }\n protected _hashTypedData(structHash: `0x${string}` | string): `0x${string}` {\n return keccak256(encodePacked(['string', 'bytes32', 'bytes32'], ['\\x19\\x01', this._domainSeparator(), structHash as `0x${string}`]));\n }\n hashTypedData(structHash: `0x${string}` | string): string {\n return this._hashTypedData(structHash);\n }"
}
}
]
}
7 changes: 4 additions & 3 deletions transpiler/runtime/ECDSA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { keccak256, toHex, fromHex } from 'viem';
import { Contract } from './index';
import { Contract } from './base';

// Constants from the original Solidity
const N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
Expand Down Expand Up @@ -49,9 +49,10 @@ export class ECDSA extends Contract {
/**
* Recovers the signer's address from a message digest hash and signature.
* Throws InvalidSignature error on recovery failure.
* Note: Accepts string for compatibility with transpiled Solidity code.
*/
static recover(hash: `0x${string}`, signature: `0x${string}`): string {
const result = ECDSA.tryRecover(hash, signature);
static recover(hash: `0x${string}` | string, signature: `0x${string}` | string): string {
const result = ECDSA.tryRecover(hash as `0x${string}`, signature as `0x${string}`);
if (result === ZERO_ADDRESS) {
throw new Error("InvalidSignature");
}
Expand Down
172 changes: 172 additions & 0 deletions transpiler/runtime/EIP712.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* EIP712 - Typed structured data hashing and signing
*
* This is a TypeScript implementation of Solady's EIP712 pattern.
* Provides domain separator computation and typed data hashing for EIP-712.
*
* @see transpiler/runtime-replacements.json for configuration
*/

import { keccak256, encodeAbiParameters, concat, toHex } from 'viem';
import { Contract, ADDRESS_ZERO } from './base';

// EIP-712 Domain Type Hash
// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
const DOMAIN_TYPEHASH = '0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f' as `0x${string}`;

/**
* Abstract base class for EIP-712 typed structured data hashing and signing.
* Contracts that need EIP-712 functionality should extend this class and
* implement _domainNameAndVersion().
*/
export abstract class EIP712 extends Contract {
private _cachedNameHash: `0x${string}` = '0x0000000000000000000000000000000000000000000000000000000000000000';
private _cachedVersionHash: `0x${string}` = '0x0000000000000000000000000000000000000000000000000000000000000000';
private _cachedDomainSeparator: `0x${string}` = '0x0000000000000000000000000000000000000000000000000000000000000000';
private _cachedChainId: bigint = 1n; // Default to mainnet for simulation

constructor(address?: string) {
super(address);
this._initializeEIP712();
}

/**
* Initialize the cached domain separator values
*/
private _initializeEIP712(): void {
const [name, version] = this._domainNameAndVersion();

// Hash the name and version
this._cachedNameHash = keccak256(
encodeAbiParameters([{ type: 'string' }], [name])
);
this._cachedVersionHash = keccak256(
encodeAbiParameters([{ type: 'string' }], [version])
);

// Build and cache the domain separator
this._cachedDomainSeparator = this._buildDomainSeparator();
}

/**
* Override this to return the domain name and version.
* @returns Tuple of [name, version]
*/
protected abstract _domainNameAndVersion(): [string, string];

/**
* Override this if the domain name and version may change after deployment.
* Default: false
*/
protected _domainNameAndVersionMayChange(): boolean {
return false;
}

/**
* Returns the EIP-712 domain separator.
*/
protected _domainSeparator(): `0x${string}` {
if (this._domainNameAndVersionMayChange()) {
return this._buildDomainSeparator();
}
return this._cachedDomainSeparator;
}

/**
* Returns the hash of the fully encoded EIP-712 message for this domain.
* The hash can be used together with ECDSA.recover to obtain the signer.
*/
protected _hashTypedData(structHash: `0x${string}` | string): `0x${string}` {
const domainSeparator = this._domainSeparator();

// EIP-712: "\x19\x01" ++ domainSeparator ++ structHash
// The prefix \x19\x01 is used to prevent collision with eth_sign
const encoded = concat([
'0x1901' as `0x${string}`,
domainSeparator,
structHash as `0x${string}`
]);

return keccak256(encoded);
}

/**
* Build the domain separator from cached or computed values.
*/
private _buildDomainSeparator(): `0x${string}` {
let nameHash: `0x${string}`;
let versionHash: `0x${string}`;

if (this._domainNameAndVersionMayChange()) {
const [name, version] = this._domainNameAndVersion();
nameHash = keccak256(encodeAbiParameters([{ type: 'string' }], [name]));
versionHash = keccak256(encodeAbiParameters([{ type: 'string' }], [version]));
} else {
nameHash = this._cachedNameHash;
versionHash = this._cachedVersionHash;
}

// Domain separator = keccak256(abi.encode(DOMAIN_TYPEHASH, nameHash, versionHash, chainId, address(this)))
const encoded = encodeAbiParameters(
[
{ type: 'bytes32' },
{ type: 'bytes32' },
{ type: 'bytes32' },
{ type: 'uint256' },
{ type: 'address' }
],
[
DOMAIN_TYPEHASH,
nameHash,
versionHash,
this._cachedChainId,
this._contractAddress as `0x${string}`
]
);

return keccak256(encoded);
}

/**
* Set the chain ID for domain separator calculation
*/
setChainId(chainId: bigint): void {
this._cachedChainId = chainId;
// Rebuild domain separator with new chain ID
if (!this._domainNameAndVersionMayChange()) {
this._cachedDomainSeparator = this._buildDomainSeparator();
}
}

/**
* EIP-5267: Returns the domain information
*/
eip712Domain(): {
fields: string;
name: string;
version: string;
chainId: bigint;
verifyingContract: string;
salt: string;
extensions: bigint[];
} {
const [name, version] = this._domainNameAndVersion();
return {
fields: '0x0f', // `0b01111` - name, version, chainId, verifyingContract
name,
version,
chainId: this._cachedChainId,
verifyingContract: this._contractAddress,
salt: '0x0000000000000000000000000000000000000000000000000000000000000000',
extensions: []
};
}

/**
* Helper to hash typed data externally (for use by other contracts)
* Note: Return type is string for compatibility with transpiled Solidity code
*/
hashTypedData(structHash: `0x${string}` | string): string {
return this._hashTypedData(structHash);
}
}
2 changes: 1 addition & 1 deletion transpiler/runtime/EnumerableSetLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @see transpiler/runtime-replacements.json for configuration
*/

import { Contract } from './index';
import { Contract } from './base';

// =============================================================================
// SET TYPE CLASSES
Expand Down
2 changes: 1 addition & 1 deletion transpiler/runtime/Ownable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see transpiler/runtime-replacements.json for configuration
*/

import { Contract, ADDRESS_ZERO } from './index';
import { Contract, ADDRESS_ZERO } from './base';

/**
* Simple single owner authorization mixin.
Expand Down
Loading