Skip to content
Open
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
5 changes: 5 additions & 0 deletions typescript/.changeset/add-erc8021-builder-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@x402/evm": minor
---

Add optional ERC-8021 Builder Code attribution to settlement transactions
59 changes: 59 additions & 0 deletions typescript/packages/mechanisms/evm/src/erc8021.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { encodeFunctionData, type Hex } from "viem";
import type { FacilitatorEvmSigner } from "./signer";

// ERC-8021 trailing marker (16 bytes) + schema byte
const ERC_8021_MARKER = "8021000000000000000000008021";
const ERC_8021_SCHEMA_ID = "01";

/**
* Builds the raw ERC-8021 suffix: `[builderCode ASCII hex][schema 0x01][marker]`
*
* @param builderCode - UTF-8 builder code string to encode
* @returns Hex-encoded suffix string (without 0x prefix)
*/
export function buildErc8021Suffix(builderCode: string): string {
const codeHex = Buffer.from(builderCode, "utf-8").toString("hex");
return `${codeHex}${ERC_8021_SCHEMA_ID}${ERC_8021_MARKER}`;
}

/**
* Wraps `signer.writeContract` to optionally append an ERC-8021 Builder Code suffix.
* When no builderCode is provided, falls through to writeContract directly.
*
* @param signer - Facilitator signer for contract interactions
* @param writeArgs - Contract write parameters
* @param writeArgs.address - Target contract address
* @param writeArgs.abi - Contract ABI
* @param writeArgs.functionName - Function to call
* @param writeArgs.args - Function arguments
* @param writeArgs.gas - Optional gas limit
* @param builderCode - Optional ERC-8021 builder code to append
* @returns Transaction hash
*/
export async function writeContractWithBuilderCode(
signer: FacilitatorEvmSigner,
writeArgs: {
address: `0x${string}`;
abi: readonly unknown[];
functionName: string;
args: readonly unknown[];
gas?: bigint;
},
builderCode?: string,
): Promise<Hex> {
if (!builderCode) {
return signer.writeContract(writeArgs);
}

const calldata = encodeFunctionData({
abi: writeArgs.abi as readonly Record<string, unknown>[],
functionName: writeArgs.functionName,
args: [...writeArgs.args],
});

const suffix = buildErc8021Suffix(builderCode);
return signer.sendTransaction({
to: writeArgs.address,
data: `${calldata}${suffix}` as Hex,
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PaymentRequirements, VerifyResponse } from "@x402/core/types";
import { encodeFunctionData, getAddress, Hex, parseErc6492Signature, parseSignature } from "viem";
import { writeContractWithBuilderCode } from "../../erc8021";
import { eip3009ABI } from "../../constants";
import { multicall, ContractCall, RawContractCall } from "../../multicall";
import { FacilitatorEvmSigner } from "../../signer";
Expand Down Expand Up @@ -195,12 +196,14 @@ export async function diagnoseEip3009SimulationFailure(
* @param erc20Address - ERC-20 token contract address
* @param payload - EIP-3009 transfer authorization payload
*
* @param builderCode - Optional ERC-8021 builder code to append to calldata
* @returns Transaction hash
*/
export async function executeTransferWithAuthorization(
signer: FacilitatorEvmSigner,
erc20Address: `0x${string}`,
payload: ExactEIP3009Payload,
builderCode?: string,
): Promise<Hex> {
const { signature } = parseErc6492Signature(payload.signature!);
const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length;
Expand All @@ -218,23 +221,31 @@ export async function executeTransferWithAuthorization(

if (isECDSA) {
const parsedSig = parseSignature(signature);
return signer.writeContract({
return writeContractWithBuilderCode(
signer,
{
address: erc20Address,
abi: eip3009ABI,
functionName: "transferWithAuthorization",
args: [
...baseArgs,
(parsedSig.v as number | undefined) || parsedSig.yParity,
parsedSig.r,
parsedSig.s,
],
},
builderCode,
);
}

return writeContractWithBuilderCode(
signer,
{
address: erc20Address,
abi: eip3009ABI,
functionName: "transferWithAuthorization",
args: [
...baseArgs,
(parsedSig.v as number | undefined) || parsedSig.yParity,
parsedSig.r,
parsedSig.s,
],
});
}

return signer.writeContract({
address: erc20Address,
abi: eip3009ABI,
functionName: "transferWithAuthorization",
args: [...baseArgs, signature],
});
args: [...baseArgs, signature],
},
builderCode,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface EIP3009FacilitatorConfig {
* @default false
*/
simulateInSettle?: boolean;
/** ERC-8021 Builder Code to append to settlement calldata. */
builderCode?: string;
}

/**
Expand Down Expand Up @@ -297,6 +299,7 @@ export async function settleEIP3009(
signer,
getAddress(requirements.asset),
eip3009Payload,
config.builderCode,
);

// Wait for transaction confirmation
Expand Down
79 changes: 55 additions & 24 deletions typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type Erc20ApprovalGasSponsoringSigner,
} from "../extensions";
import { getAddress, encodeFunctionData } from "viem";
import { writeContractWithBuilderCode, buildErc8021Suffix } from "../../erc8021";
import {
PERMIT2_ADDRESS,
permit2WitnessTypes,
Expand Down Expand Up @@ -58,6 +59,8 @@ export interface Permit2FacilitatorConfig {
* @default false
*/
simulateInSettle?: boolean;
/** ERC-8021 Builder Code to append to settlement calldata. */
builderCode?: string;
}

/**
Expand Down Expand Up @@ -353,10 +356,19 @@ export async function settlePermit2(
};
}

const builderCode = config?.builderCode;

// Branch: EIP-2612 gas sponsoring (atomic settleWithPermit via contract)
const eip2612Info = extractEip2612GasSponsoringInfo(payload);
if (eip2612Info) {
return settlePermit2WithEIP2612(exactProxyConfig, signer, payload, permit2Payload, eip2612Info);
return settlePermit2WithEIP2612(
exactProxyConfig,
signer,
payload,
permit2Payload,
eip2612Info,
builderCode,
);
}

// Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer)
Expand All @@ -377,12 +389,13 @@ export async function settlePermit2(
payload,
permit2Payload,
erc20Info,
builderCode,
);
}
}

// Branch: standard settle (allowance already on-chain)
return settlePermit2Direct(exactProxyConfig, signer, payload, permit2Payload);
return settlePermit2Direct(exactProxyConfig, signer, payload, permit2Payload, builderCode);
}

// ---------------------------------------------------------------------------
Expand All @@ -397,6 +410,7 @@ export async function settlePermit2(
* @param payload - The payment payload for network info
* @param permit2Payload - The Permit2 payload with authorization and signature
* @param eip2612Info - The EIP-2612 gas sponsoring info from the payload extension
* @param builderCode - Optional ERC-8021 builder code to append to calldata
* @returns Promise resolving to a settlement response
*/
async function settlePermit2WithEIP2612(
Expand All @@ -405,26 +419,31 @@ async function settlePermit2WithEIP2612(
payload: PaymentPayload,
permit2Payload: ExactPermit2Payload,
eip2612Info: Eip2612GasSponsoringInfo,
builderCode?: string,
): Promise<SettleResponse> {
const payer = permit2Payload.permit2Authorization.from;
try {
const { v, r, s } = splitEip2612Signature(eip2612Info.signature);

const tx = await signer.writeContract({
address: config.proxyAddress,
abi: config.proxyABI,
functionName: "settleWithPermit",
args: [
{
value: BigInt(eip2612Info.amount),
deadline: BigInt(eip2612Info.deadline),
r,
s,
v,
},
...buildExactPermit2SettleArgs(permit2Payload),
],
});
const tx = await writeContractWithBuilderCode(
signer,
{
address: config.proxyAddress,
abi: config.proxyABI,
functionName: "settleWithPermit",
args: [
{
value: BigInt(eip2612Info.amount),
deadline: BigInt(eip2612Info.deadline),
r,
s,
v,
},
...buildExactPermit2SettleArgs(permit2Payload),
],
},
builderCode,
);

return waitAndReturnSettleResponse(signer, tx, payload, payer);
} catch (error) {
Expand All @@ -441,6 +460,7 @@ async function settlePermit2WithEIP2612(
* @param permit2Payload - The Permit2 payload with authorization and signature
* @param erc20Info - Object containing the signed approval transaction
* @param erc20Info.signedTransaction - The RLP-encoded signed ERC-20 approve transaction
* @param builderCode - Optional ERC-8021 builder code to append to calldata
* @returns Promise resolving to a settlement response
*/
async function settlePermit2WithERC20Approval(
Expand All @@ -449,16 +469,21 @@ async function settlePermit2WithERC20Approval(
payload: PaymentPayload,
permit2Payload: ExactPermit2Payload,
erc20Info: { signedTransaction: string },
builderCode?: string,
): Promise<SettleResponse> {
const payer = permit2Payload.permit2Authorization.from;

try {
const settleData = encodeFunctionData({
let settleData = encodeFunctionData({
abi: config.proxyABI,
functionName: "settle",
args: buildExactPermit2SettleArgs(permit2Payload),
});

if (builderCode) {
settleData = `${settleData}${buildErc8021Suffix(builderCode)}` as `0x${string}`;
}

const txHashes = await extensionSigner.sendTransactions([
erc20Info.signedTransaction as `0x${string}`,
{ to: config.proxyAddress, data: settleData, gas: BigInt(300_000) },
Expand All @@ -478,22 +503,28 @@ async function settlePermit2WithERC20Approval(
* @param signer - The facilitator signer for contract writes
* @param payload - The payment payload for network info
* @param permit2Payload - The Permit2 payload with authorization and signature
* @param builderCode - Optional ERC-8021 builder code to append to calldata
* @returns Promise resolving to a settlement response
*/
async function settlePermit2Direct(
config: Permit2ProxyConfig,
signer: FacilitatorEvmSigner,
payload: PaymentPayload,
permit2Payload: ExactPermit2Payload,
builderCode?: string,
): Promise<SettleResponse> {
const payer = permit2Payload.permit2Authorization.from;
try {
const tx = await signer.writeContract({
address: config.proxyAddress,
abi: config.proxyABI,
functionName: "settle",
args: buildExactPermit2SettleArgs(permit2Payload),
});
const tx = await writeContractWithBuilderCode(
signer,
{
address: config.proxyAddress,
abi: config.proxyABI,
functionName: "settle",
args: buildExactPermit2SettleArgs(permit2Payload),
},
builderCode,
);

return waitAndReturnSettleResponse(signer, tx, payload, payer);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export interface ExactEvmSchemeConfig {
* @default false
*/
simulateInSettle?: boolean;
/**
* ASCII Builder Code to append as an ERC-8021 suffix on settlement transactions.
* When set, settlement calldata is extended with the builder attribution tag.
*/
builderCode?: string;
}

/**
Expand All @@ -51,6 +56,7 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator {
this.config = {
deployERC4337WithEIP6492: config?.deployERC4337WithEIP6492 ?? false,
simulateInSettle: config?.simulateInSettle ?? false,
builderCode: config?.builderCode ?? "",
};
}

Expand Down Expand Up @@ -117,6 +123,7 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator {
if (isPermit2) {
return settlePermit2(this.signer, payload, requirements, rawPayload, context, {
simulateInSettle: this.config.simulateInSettle,
builderCode: this.config.builderCode || undefined,
});
}

Expand Down
3 changes: 3 additions & 0 deletions typescript/packages/mechanisms/evm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export { UptoEvmScheme } from "./upto";
export type { UptoPermit2Payload, UptoPermit2Witness, UptoPermit2Authorization } from "./types";
export { isUptoPermit2Payload } from "./types";

// ERC-8021 Builder Code
export { buildErc8021Suffix } from "./erc8021";

// Constants
export {
PERMIT2_ADDRESS,
Expand Down
Loading
Loading