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
12 changes: 3 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,11 @@ console.log("Proxy redeem completed:", proxyResult.transactionHash);

### Deposit Wallet

Deposit Wallets are UUPS-upgradeable smart contract wallets that support EIP-712 signed batch execution. Unlike Safe and Proxy wallets which use the `execute()` method, Deposit Wallets have dedicated methods.
Deposit Wallets are smart contract wallets that support EIP-712 signed batch execution. Unlike Safe and Proxy wallets which use the `execute()` method, Deposit Wallets have dedicated methods.

#### Derive Deposit Wallet Address

You can predict the deposit wallet address before deployment using CREATE2:
You can predict the deposit wallet address before deployment:

```typescript
const client = new RelayClient(relayerUrl, chainId, wallet, builderConfig);
Expand All @@ -326,13 +326,7 @@ const walletAddress = await client.deriveDepositWalletAddress();
console.log("Expected deposit wallet address:", walletAddress);
```

Or use the standalone function directly:

```typescript
import { deriveDepositWallet } from "@polymarket/builder-relayer-client";

const walletAddress = deriveDepositWallet(ownerAddress, factoryAddress, implementationAddress);
```
The standalone `deriveDepositWallet()` helper only derives UUPS deposit wallet addresses and is deprecated. Prefer `client.deriveDepositWalletAddress()`.

#### Deploy Deposit Wallet

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"ws": "^8.11.0"
},
"dependencies": {
"@ethersproject/providers": "5.8.0",
"@ethersproject/wallet": "5.8.0",
"@polymarket/builder-abstract-signer": "0.0.1",
"@polymarket/builder-signing-sdk": "^0.0.8",
"axios": "^0.27.2",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 59 additions & 7 deletions src/builder/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const deriveSafe = (address: string, safeFactory: string) : string => {
const ERC1967_CONST1: Hex = "0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3";
const ERC1967_CONST2: Hex = "0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076";
const ERC1967_PREFIX = 0x61003d3d8160233d3973n;
const ERC1967_BEACON_CONST1: Hex = "0xb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3";
const ERC1967_BEACON_CONST2: Hex = "0x1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6c";
const ERC1967_BEACON_CONST3: Hex = "0x60195155f3363d3d373d3d363d602036600436635c60da";
const ERC1967_BEACON_PREFIX = 0x6100523d8160233d3973n;

/**
* Replicates Solady LibClone.initCodeHashERC1967(implementation, args).
Expand All @@ -46,21 +50,69 @@ function initCodeHashERC1967(implementation: Address, args: Hex): Hex {
}

/**
* Computes the deterministic deposit wallet address for a given owner.
* Replicates Solady LibClone.initCodeHashERC1967Beacon(beacon, args).
*/
function initCodeHashERC1967Beacon(beacon: Address, args: Hex): Hex {
const n = BigInt((args.length - 2) / 2);
const combined = ERC1967_BEACON_PREFIX + (n << 56n);

return keccak256(
concat([
toHex(combined, { size: 10 }),
beacon as Hex,
ERC1967_BEACON_CONST3,
ERC1967_BEACON_CONST2,
ERC1967_BEACON_CONST1,
args,
]),
);
}

function depositWalletArgs(owner: string, factory: string): Hex {
const walletId = pad(owner as Hex, { dir: "left", size: 32 });
return encodeAbiParameters(
[{ type: "address" }, { type: "bytes32" }],
[factory as Address, walletId],
);
}

/**
* Computes the deterministic UUPS deposit wallet address for a given owner.
* walletId is derived as bytes32(owner) - the 20-byte address left-padded to 32 bytes.
*/
export const deriveDepositWallet = (
export const deriveUupsDepositWallet = (
owner: string,
factory: string,
implementation: string,
): string => {
const walletId = pad(owner as Hex, { dir: "left", size: 32 });
const args = encodeAbiParameters(
[{ type: "address" }, { type: "bytes32" }],
[factory as Address, walletId],
);
const args = depositWalletArgs(owner, factory);
const salt = keccak256(args);
const bytecodeHash = initCodeHashERC1967(implementation as Address, args);

return getCreate2Address({ from: factory as Hex, salt, bytecodeHash });
};

/**
* Computes the deterministic UUPS deposit wallet address for a given owner.
* @deprecated Use RelayClient.deriveDepositWalletAddress(). This helper only derives UUPS deposit wallet addresses.
*/
export const deriveDepositWallet = (
owner: string,
factory: string,
implementation: string,
): string => deriveUupsDepositWallet(owner, factory, implementation);

/**
* Computes the deterministic beacon deposit wallet address for a given owner.
*/
export const deriveBeaconDepositWallet = (
owner: string,
factory: string,
beacon: string,
): string => {
const args = depositWalletArgs(owner, factory);
const salt = keccak256(args);
const bytecodeHash = initCodeHashERC1967Beacon(beacon as Address, args);

return getCreate2Address({ from: factory as Hex, salt, bytecodeHash });
};
2 changes: 1 addition & 1 deletion src/builder/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./safe";
export * from "./create";
export * from "./derive";
export { deriveDepositWallet, deriveProxyWallet, deriveSafe } from "./derive";
export * from "./proxy";
export * from "./deposit-wallet";
81 changes: 78 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { Wallet } from "@ethersproject/wallet";
import { JsonRpcSigner } from "@ethersproject/providers";
import { WalletClient, zeroAddress } from "viem";
import {
BaseError,
ContractFunctionRevertedError,
createPublicClient,
ExecutionRevertedError,
http,
RawContractError,
type Chain,
type PublicClient,
WalletClient,
zeroAddress,
} from "viem";
import { polygon } from "viem/chains";
import { createAbstractSigner, IAbstractSigner } from "@polymarket/builder-abstract-signer";
import {
GET,
Expand Down Expand Up @@ -42,15 +54,39 @@ import {
buildDepositWalletBatchRequest,
buildDepositWalletCreateRequest,
deriveSafe,
deriveDepositWallet,
} from "./builder";
import { deriveBeaconDepositWallet, deriveUupsDepositWallet } from "./builder/derive";
import { sleep } from "./utils";
import { ClientRelayerTransactionResponse } from "./response";
import { ContractConfig, getContractConfig, isProxyContractConfigValid, isSafeContractConfigValid, isDepositWalletContractConfigValid } from "./config";
import { BuilderConfig, BuilderHeaderPayload } from "@polymarket/builder-signing-sdk";
import { CONFIG_UNSUPPORTED_ON_CHAIN, SAFE_DEPLOYED, SAFE_NOT_DEPLOYED, SIGNER_UNAVAILABLE } from "./errors";
import { encodeProxyTransactionData } from "./encode";

const FACTORY_BEACON_SELECTOR = "0x49493a4d";

function decodeAddressReturnData(data?: string): string {
if (data === undefined || data.length < 66) {
return zeroAddress;
}
return `0x${data.slice(-40)}`;
}

function isContractRevert(error: unknown): boolean {
if (!(error instanceof BaseError)) {
return false;
}

return error.walk((err) => (
err instanceof ContractFunctionRevertedError ||
err instanceof ExecutionRevertedError ||
(err instanceof RawContractError && err.code === 3)
)) !== null;
}

export interface RelayClientOptions {
chain?: Chain;
}

export class RelayClient {
readonly relayerUrl: string;
Expand All @@ -63,6 +99,8 @@ export class RelayClient {

readonly httpClient: HttpClient;

private readonly publicClient: PublicClient;

readonly signer?: IAbstractSigner;

readonly builderConfig?: BuilderConfig;
Expand All @@ -73,6 +111,7 @@ export class RelayClient {
signer?: Wallet | JsonRpcSigner | WalletClient,
builderConfig?: BuilderConfig,
relayTxType?: RelayerTxType,
options?: RelayClientOptions,
) {
this.relayerUrl = relayerUrl.endsWith("/") ? relayerUrl.slice(0, -1) : relayerUrl;
this.chainId = chainId;
Expand All @@ -82,6 +121,14 @@ export class RelayClient {
this.relayTxType = relayTxType;
this.contractConfig = getContractConfig(chainId);
this.httpClient = new HttpClient();
const chain = options?.chain ?? polygon;
if (chain.id !== chainId) {
throw new Error("chain id does not match chainId");
}
Comment thread
cesarenaldi marked this conversation as resolved.
this.publicClient = createPublicClient({
chain,
transport: http(),
});

if (signer != undefined) {
this.signer = createAbstractSigner(chainId, signer);
Expand Down Expand Up @@ -384,7 +431,35 @@ export class RelayClient {
throw CONFIG_UNSUPPORTED_ON_CHAIN;
}
const address = await (this.signer as IAbstractSigner).getAddress();
return deriveDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation);
const uupsAddress = deriveUupsDepositWallet(address, config.DepositWalletFactory, config.DepositWalletImplementation);
const beacon = await this.getDepositWalletFactoryBeacon(config.DepositWalletFactory);
if (beacon.toLowerCase() === zeroAddress) {
return uupsAddress;
}
if (await this.isContractDeployed(uupsAddress)) {
return uupsAddress;
}
return deriveBeaconDepositWallet(address, config.DepositWalletFactory, beacon);
}

private async isContractDeployed(address: string): Promise<boolean> {
const code = await this.publicClient.getCode({ address: address as `0x${string}` });
return code !== undefined && code !== "0x";
}

private async getDepositWalletFactoryBeacon(factory: string): Promise<string> {
try {
const { data } = await this.publicClient.call({
to: factory as `0x${string}`,
data: FACTORY_BEACON_SELECTOR,
});
return decodeAddressReturnData(data);
} catch (error) {
if (isContractRevert(error)) {
return zeroAddress;
}
throw error;
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@ export const getContractConfig = (chainId: number): ContractConfig => {
default:
throw new Error("Invalid network");
}
};
};
96 changes: 93 additions & 3 deletions tests/signatures/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import { Wallet } from "ethers";
import { JsonRpcProvider } from "@ethersproject/providers";

import { createWalletClient, http, WalletClient, zeroAddress } from "viem";
import { polygon } from "viem/chains";
import { polygon, polygonAmoy } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { encodeProxyTransactionData } from "../../src/encode";
import { buildProxyTransactionRequest, buildSafeCreateTransactionRequest, buildSafeTransactionRequest } from "../../src/builder";
import { RelayClient } from "../../src/client";
import {
buildProxyTransactionRequest,
buildSafeCreateTransactionRequest,
buildSafeTransactionRequest,
} from "../../src/builder";
import { deriveBeaconDepositWallet, deriveUupsDepositWallet } from "../../src/builder/derive";
import {
CallType,
OperationType,
Expand Down Expand Up @@ -37,6 +43,7 @@ describe("setup", () => {
// Calldata to approve CTF as spender on USDC
const usdc = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
const approveCalldata = "0x095ea7b30000000000000000000000004d97dcd97ec945f40cf65f87097ace5ea0476045ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
const depositWalletBeacon = "0x7A18EDfe055488A3128f01F563e5B479D92ffc3a";

// ethers signer
const w = new Wallet(privateKey);
Expand Down Expand Up @@ -160,4 +167,87 @@ describe("setup", () => {

});
});
});

describe("derive deposit wallet address", () => {
it("derives the UUPS deposit wallet address", () => {
const wallet = deriveUupsDepositWallet(
"0x0000000000000000000000000000000000000001",
contractConfig.DepositWalletContracts.DepositWalletFactory,
contractConfig.DepositWalletContracts.DepositWalletImplementation,
);

expect(wallet.toLowerCase()).equal("0x57ffbc34de23124faeb8387fcd689d314e57accd");
});

it("derives the beacon deposit wallet address", () => {
const wallet = deriveBeaconDepositWallet(
"0x0000000000000000000000000000000000000001",
contractConfig.DepositWalletContracts.DepositWalletFactory,
depositWalletBeacon,
);

expect(wallet.toLowerCase()).equal("0x94bf330955a0b957662feaf878de77bf25f76cd9");
});

it("uses factory beacon detection for the client expected address", async () => {
const client = new RelayClient("http://localhost:8080", chainId, ethersWallet);
(client as unknown as { publicClient: { call: () => Promise<{ data: string }>; getCode: () => Promise<undefined> } }).publicClient = {
call: async () => ({ data: `0x000000000000000000000000${depositWalletBeacon.slice(2)}` }),
getCode: async () => undefined,
};

const wallet = await client.deriveDepositWalletAddress();
const expectedWallet = deriveBeaconDepositWallet(
address,
contractConfig.DepositWalletContracts.DepositWalletFactory,
depositWalletBeacon,
);

expect(wallet).equal(expectedWallet);
});

it("falls back to the UUPS address when the factory has no beacon", async () => {
const client = new RelayClient("http://localhost:8080", chainId, ethersWallet);
(client as unknown as { publicClient: { call: () => Promise<{ data: string }> } }).publicClient = {
call: async () => ({ data: `0x000000000000000000000000${zeroAddress.slice(2)}` }),
};

const wallet = await client.deriveDepositWalletAddress();
const expectedWallet = deriveUupsDepositWallet(
address,
contractConfig.DepositWalletContracts.DepositWalletFactory,
contractConfig.DepositWalletContracts.DepositWalletImplementation,
);

expect(wallet).equal(expectedWallet);
});

it("returns the UUPS address when it is already deployed", async () => {
const client = new RelayClient("http://localhost:8080", chainId, ethersWallet);
(client as unknown as { publicClient: { call: () => Promise<{ data: string }>; getCode: () => Promise<string> } }).publicClient = {
call: async () => ({ data: `0x000000000000000000000000${depositWalletBeacon.slice(2)}` }),
getCode: async () => "0x01",
};

const wallet = await client.deriveDepositWalletAddress();
const expectedWallet = deriveUupsDepositWallet(
address,
contractConfig.DepositWalletContracts.DepositWalletFactory,
contractConfig.DepositWalletContracts.DepositWalletImplementation,
);

expect(wallet).equal(expectedWallet);
});

it("rejects an options chain that does not match the chain id", () => {
expect(() => new RelayClient(
"http://localhost:8080",
chainId,
ethersWallet,
undefined,
undefined,
{ chain: polygonAmoy },
)).to.throw("chain id does not match chainId");
});
});
});
Loading