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
22 changes: 22 additions & 0 deletions .changeset/chubby-peas-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@turnkey/gas-station": major
---

Updated the `@turnkey/gas-station` SDK to align with the audited smart contract changes. The audit resulted in several interface updates:

**Contract Changes:**
- **New contract addresses**: Updated both delegate and execution contract addresses to the newly deployed versions
- **EIP-712 field name changes**: The canonical delegate contract interface uses simplified field names (`to`, `value`, `data`) instead of the previous descriptive names (`outputContract`, `ethAmount`, `arguments`)

**SDK Updates:**
- Updated `DEFAULT_EXECUTION_CONTRACT` address from `0x4ece92b06C7d2d99d87f052E0Fca47Fb180c3348` to `0x00000000008c57a1CE37836a5e9d36759D070d8c`
- Updated `DEFAULT_DELEGATE_CONTRACT` address from `0xC2a37Ee08cAc3778d9d05FF0a93FD5B553C77E3a` to `0x000066a00056CD44008768E2aF00696e19A30084`
- Updated EIP-712 Execution typehash field names to match the contract's canonical interface
- Updated EIP-712 ApproveThenExecute typehash field names to match the contract's canonical interface
- Updated Turnkey policy conditions in `buildIntentSigningPolicy` to reference the new field names (`to`, `value` instead of `outputContract`, `ethAmount`)
- Updated documentation and examples to reflect the new field names

**Files Modified:**
- `packages/gas-station/src/config.ts` - Updated contract addresses
- `packages/gas-station/src/intentBuilder.ts` - Updated EIP-712 type definitions and message objects
- `packages/gas-station/src/policyUtils.ts` - Updated policy condition field references and documentation
48 changes: 23 additions & 25 deletions packages/gas-station/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Turnkey Gas Station SDK

> **⚠️ BETA WARNING**: This SDK is currently in beta. The underlying smart contracts are **unaudited** and should not be used in production environments. Use at your own risk.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹


A reusable SDK for implementing gasless transactions using EIP-7702, Turnkey wallet management, and your own paymaster. This package provides clean abstractions and utility methods to quickly integrate with Turnkey's contracts for sponsored transaction execution.

## What is This?
Expand Down Expand Up @@ -345,8 +343,8 @@ import { buildIntentSigningPolicy } from "@turnkey/gas-station";

// USDC-only policy
const eoaPolicy = buildIntentSigningPolicy({
organizationId: "your-org-id",
eoaUserId: "user-id",
organizationId: "a5b89e4f-1234-5678-9abc-def012345678",
eoaUserId: "3c7d6e8a-4b5c-6d7e-8f9a-0b1c2d3e4f5a",
restrictions: {
allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"], // USDC on Base
disallowEthTransfer: true, // Disallow ETH transfers
Expand All @@ -356,15 +354,15 @@ const eoaPolicy = buildIntentSigningPolicy({

// Resulting policy restricts signing to USDC transfers only:
// {
// organizationId: "your-org-id",
// organizationId: "a5b89e4f-1234-5678-9abc-def012345678",
// policyName: "USDC Only Policy",
// effect: "EFFECT_ALLOW",
// consensus: "approvers.any(user, user.id == 'user-id')",
// consensus: "approvers.any(user, user.id == '3c7d6e8a-4b5c-6d7e-8f9a-0b1c2d3e4f5a')",
// condition: "activity.resource == 'PRIVATE_KEY' && " +
// "activity.action == 'SIGN' && " +
// "eth.eip_712.primary_type == 'Execution' && " +
// "(eth.eip_712.message['outputContract'] == '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') && " +
// "eth.eip_712.message['ethAmount'] == '0'",
// "(eth.eip_712.message['to'] == '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') && " +
// "eth.eip_712.message['value'] == '0'",
// notes: "Restricts which EIP-712 intents the EOA can sign for gas station execution"
// }
```
Expand Down Expand Up @@ -392,8 +390,8 @@ await ensureGasStationInterface(

// Paymaster protection policy with ETH amount limit
const paymasterPolicy = buildPaymasterExecutionPolicy({
organizationId: "paymaster-org-id",
paymasterUserId: "paymaster-user-id",
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
paymasterUserId: "8f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
restrictions: {
allowedEOAs: ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"],
Expand All @@ -407,23 +405,23 @@ const paymasterPolicy = buildPaymasterExecutionPolicy({

// Resulting policy uses ABI parsing for direct argument access:
// {
// organizationId: "paymaster-org-id",
// organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
// policyName: "Paymaster Protection",
// effect: "EFFECT_ALLOW",
// consensus: "approvers.any(user, user.id == 'paymaster-user-id')",
// consensus: "approvers.any(user, user.id == '8f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c')",
// condition: "activity.resource == 'PRIVATE_KEY' && " +
// "activity.action == 'SIGN' && " +
// "eth.tx.to == '0xe511ad0a281c10b8408381e2ab8525abe587827b' && " +
// "eth.tx.to == '0x00000000008c57a1ce37836a5e9d36759d070d8c' && " +
// "(eth.tx.contract_call_args['_to'] == '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') && " +
// "(eth.tx.contract_call_args['_targetEoA'] == '0x742d35cc6634c0532925a3b844bc9e7595f0beb') && " +
// "eth.tx.contract_call_args['ethAmount'] <= 100000000000000000 && " +
// "(eth.tx.contract_call_args['_target'] == '0x742d35cc6634c0532925a3b844bc9e7595f0beb') && " +
// "eth.tx.contract_call_args['_ethAmount'] <= 100000000000000000 && " +
// "eth.tx.gasPrice <= 50000000000 && " +
// "eth.tx.gas <= 500000",
// notes: "Restricts which transactions the paymaster can execute on the gas station"
// }
```

**Note:** The `ensureGasStationInterface()` function uploads the Gas Station ABI to Turnkey's Smart Contract Interface feature. This enables Turnkey's policy engine to parse the ABI-encoded transaction data and directly compare the `ethAmount` parameter as a uint256 value, rather than raw bytes. The function checks if the ABI already exists before uploading to avoid duplicates.
**Note:** The `ensureGasStationInterface()` function uploads the Gas Station ABI to Turnkey's Smart Contract Interface feature. This enables Turnkey's policy engine to parse the ABI-encoded transaction data and directly compare the `_ethAmount` parameter as a uint256 value, rather than raw bytes. The function checks if the ABI already exists before uploading to avoid duplicates.

#### Defense in Depth

Expand All @@ -439,8 +437,8 @@ import { parseGwei } from "viem";

// Layer 1: EOA can only sign USDC intents
const eoaPolicy = buildIntentSigningPolicy({
organizationId: "user-org",
eoaUserId: "user-id",
organizationId: "a5b89e4f-1234-5678-9abc-def012345678",
eoaUserId: "3c7d6e8a-4b5c-6d7e-8f9a-0b1c2d3e4f5a",
restrictions: {
allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
disallowEthTransfer: true, // No ETH transfers
Expand All @@ -449,8 +447,8 @@ const eoaPolicy = buildIntentSigningPolicy({

// Layer 2: Paymaster can only execute for specific users with gas limits
const paymasterPolicy = buildPaymasterExecutionPolicy({
organizationId: "paymaster-org",
paymasterUserId: "paymaster-user-id",
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
paymasterUserId: "8f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
restrictions: {
allowedEOAs: ["0xUserAddress..."],
Expand Down Expand Up @@ -491,7 +489,7 @@ When the paymaster signs an execution transaction calling `execute(address _targ
**Check execution contract address:**

```typescript
eth.tx.to == "0x576a4d741b96996cc93b4919a04c16545734481f";
eth.tx.to == "0x00000000008c57a1ce37836a5e9d36759d070d8c";
```

**Check which EOA is executing:**
Expand Down Expand Up @@ -530,14 +528,14 @@ Allow paymaster to execute for USDC or DAI only:

```typescript
const policy = {
organizationId: "paymaster-org-id",
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
policyName: "Stablecoin Execution Policy",
effect: "EFFECT_ALLOW",
consensus: `approvers.any(user, user.id == '${paymasterUserId}')`,
condition: [
"activity.resource == 'PRIVATE_KEY'",
"activity.action == 'SIGN'",
"eth.tx.to == '0x576a4d741b96996cc93b4919a04c16545734481f'",
"eth.tx.to == '0x00000000008c57a1ce37836a5e9d36759d070d8c'",
// Allow USDC or DAI
"(eth.tx.data[74..138] == '0000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913' || eth.tx.data[74..138] == '00000000000000000000006b175474e89094c44da98b954eedeac495271d0f')",
// Gas limits
Expand Down Expand Up @@ -568,14 +566,14 @@ const eoaConditions = approvedEOAs
.join(" || ");

const policy = {
organizationId: "paymaster-org-id",
organizationId: "f8c3a5e7-9876-5432-1abc-def098765432",
policyName: "Approved Users Only",
effect: "EFFECT_ALLOW",
consensus: `approvers.any(user, user.id == '${paymasterUserId}')`,
condition: [
"activity.resource == 'PRIVATE_KEY'",
"activity.action == 'SIGN'",
"eth.tx.to == '0x576a4d741b96996cc93b4919a04c16545734481f'",
"eth.tx.to == '0x00000000008c57a1ce37836a5e9d36759d070d8c'",
`(${eoaConditions})`,
].join(" && "),
};
Expand Down
37 changes: 7 additions & 30 deletions packages/gas-station/src/abi/gas-station.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ export const gasStationAbi = [
},
{ inputs: [], name: "ExecutionFailed", type: "error" },
{ inputs: [], name: "InvalidFunctionSelector", type: "error" },
{ inputs: [], name: "NoEthAllowed", type: "error" },
{ inputs: [], name: "NotDelegated", type: "error" },
{ stateMutability: "nonpayable", type: "fallback" },
{
inputs: [],
name: "TK_GAS_DELEGATE",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{ internalType: "address", name: "_target", type: "address" },
Expand Down Expand Up @@ -152,26 +158,6 @@ export const gasStationAbi = [
stateMutability: "view",
type: "function",
},
{
inputs: [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make explicit note of this somewhere? or no need given this wasn't really being used in the first place?

{ internalType: "address", name: "_targetEoA", type: "address" },
{ internalType: "uint128", name: "_nonce", type: "uint128" },
{
components: [
{ internalType: "address", name: "to", type: "address" },
{ internalType: "uint256", name: "value", type: "uint256" },
{ internalType: "bytes", name: "data", type: "bytes" },
],
internalType: "struct IBatchExecution.Call[]",
name: "_calls",
type: "tuple[]",
},
],
name: "hashBatchExecution",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{ internalType: "address", name: "_targetEoA", type: "address" },
Expand Down Expand Up @@ -207,7 +193,6 @@ export const gasStationAbi = [
inputs: [
{ internalType: "address", name: "_targetEoA", type: "address" },
{ internalType: "uint128", name: "_counter", type: "uint128" },
{ internalType: "address", name: "_sender", type: "address" },
],
name: "hashBurnSessionCounter",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
Expand Down Expand Up @@ -248,13 +233,6 @@ export const gasStationAbi = [
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "tkGasDelegate",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{ internalType: "address", name: "_targetEoA", type: "address" },
Expand All @@ -266,5 +244,4 @@ export const gasStationAbi = [
stateMutability: "view",
type: "function",
},
{ stateMutability: "payable", type: "receive" },
] as const;
4 changes: 2 additions & 2 deletions packages/gas-station/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { base, mainnet, sepolia } from "viem/chains";

// Default contract addresses (deterministically deployed across all chains)
export const DEFAULT_DELEGATE_CONTRACT: Hex =
"0xC2a37Ee08cAc3778d9d05FF0a93FD5B553C77E3a";
"0x000066a00056CD44008768E2aF00696e19A30084";
export const DEFAULT_EXECUTION_CONTRACT: Hex =
"0x4ece92b06C7d2d99d87f052E0Fca47Fb180c3348";
"0x00000000008c57a1CE37836a5e9d36759D070d8c";

// Type definitions
export interface GasStationConfig {
Expand Down
28 changes: 14 additions & 14 deletions packages/gas-station/src/intentBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,23 +145,23 @@ export class IntentBuilder {
verifyingContract: this.config.eoaAddress,
};

// Original: keccak256("Execution(uint128 nonce,uint32 deadline,address outputContract,uint256 ethAmount,bytes arguments)")
// keccak256("Execution(uint128 nonce,uint32 deadline,address to,uint256 value,bytes data)")
const types = {
Execution: [
{ name: "nonce", type: "uint128" },
{ name: "deadline", type: "uint32" },
{ name: "outputContract", type: "address" },
{ name: "ethAmount", type: "uint256" },
{ name: "arguments", type: "bytes" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
],
};

const message = {
nonce,
deadline,
outputContract: this.outputContract,
ethAmount: this.ethAmount,
arguments: this.callData,
to: this.outputContract,
value: this.ethAmount,
data: this.callData,
};

const signature = await this.config.eoaWalletClient.signTypedData({
Expand Down Expand Up @@ -213,17 +213,17 @@ export class IntentBuilder {
};

// Based on hashApproveThenExecute from the contract
// keccak256("ApproveThenExecute(uint128 nonce,uint32 deadline,address erc20Contract,address spender,uint256 approveAmount,address outputContract,uint256 ethAmount,bytes arguments)")
// keccak256("ApproveThenExecute(uint128 nonce,uint32 deadline,address erc20Contract,address spender,uint256 approveAmount,address to,uint256 value,bytes data)")
const types = {
ApproveThenExecute: [
{ name: "nonce", type: "uint128" },
{ name: "deadline", type: "uint32" },
{ name: "erc20Contract", type: "address" },
{ name: "spender", type: "address" },
{ name: "approveAmount", type: "uint256" },
{ name: "outputContract", type: "address" },
{ name: "ethAmount", type: "uint256" },
{ name: "arguments", type: "bytes" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
],
};

Expand All @@ -233,9 +233,9 @@ export class IntentBuilder {
erc20Contract: erc20Address,
spender,
approveAmount,
outputContract: this.outputContract,
ethAmount: this.ethAmount,
arguments: this.callData,
to: this.outputContract,
value: this.ethAmount,
data: this.callData,
};

const signature = await this.config.eoaWalletClient.signTypedData({
Expand Down
Loading
Loading