Skip to content

Commit 810c08f

Browse files
committed
Support value tracking in explicit sessions
1 parent 1dbe418 commit 810c08f

2 files changed

Lines changed: 217 additions & 30 deletions

File tree

packages/wallet/core/src/signers/session/explicit.ts

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Payload, Permission, SessionSignature, Utils } from '@0xsequence/wallet-primitives'
1+
import { Payload, Permission, SessionSignature, Constants } from '@0xsequence/wallet-primitives'
22
import { AbiFunction, AbiParameters, Address, Bytes, Hash, Hex, Provider } from 'ox'
33
import { MemoryPkStore, PkStore } from '../pk/index.js'
44
import { ExplicitSessionSigner, UsageLimit } from './session.js'
5-
import { GET_LIMIT_USAGE, INCREMENT_USAGE_LIMIT } from '../../../../primitives/dist/constants.js'
65

76
export type ExplicitParams = Omit<Permission.SessionPermissions, 'signer'>
87

8+
const VALUE_TRACKING_ADDRESS: Address.Address = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
9+
910
export class Explicit implements ExplicitSessionSigner {
1011
private readonly _privateKey: PkStore
1112

@@ -28,6 +29,27 @@ export class Explicit implements ExplicitSessionSigner {
2829
sessionManagerAddress: Address.Address,
2930
provider?: Provider.Provider,
3031
): Promise<Permission.Permission | undefined> {
32+
if (call.value !== 0n) {
33+
// Validate the value
34+
if (!provider) {
35+
throw new Error('Value transaction validation requires a provider')
36+
}
37+
const usageHash = Hash.keccak256(
38+
AbiParameters.encode(
39+
[
40+
{ type: 'address', name: 'signer' },
41+
{ type: 'address', name: 'valueTrackingAddress' },
42+
],
43+
[this.address, VALUE_TRACKING_ADDRESS],
44+
),
45+
)
46+
const { usageAmount } = await this.readCurrentUsageLimit(wallet, sessionManagerAddress, usageHash, provider)
47+
const value = Bytes.fromNumber(usageAmount + call.value, { size: 32 })
48+
if (Bytes.toBigInt(value) > this.sessionPermissions.valueLimit) {
49+
return undefined
50+
}
51+
}
52+
3153
for (const permission of this.sessionPermissions.permissions) {
3254
// Validate the permission
3355
if (await this.validatePermission(permission, call, wallet, sessionManagerAddress, provider)) {
@@ -37,12 +59,12 @@ export class Explicit implements ExplicitSessionSigner {
3759
return undefined
3860
}
3961

40-
async getCurrentUsageLimit(
62+
private async getCurrentPermissionUsageLimit(
4163
wallet: Address.Address,
4264
sessionManagerAddress: Address.Address,
4365
permission: Permission.Permission,
4466
ruleIndex: number | bigint,
45-
provider?: Provider.Provider,
67+
provider: Provider.Provider,
4668
): Promise<UsageLimit> {
4769
const encodedPermission = {
4870
target: permission.target,
@@ -54,15 +76,40 @@ export class Explicit implements ExplicitSessionSigner {
5476
mask: Bytes.toHex(rule.mask),
5577
})),
5678
}
57-
5879
const usageHash = Hash.keccak256(
5980
AbiParameters.encode(
6081
[{ type: 'address', name: 'signer' }, Permission.permissionStructAbi, { type: 'uint256', name: 'ruleIndex' }],
6182
[this.address, encodedPermission, BigInt(ruleIndex)],
6283
),
6384
)
64-
const readData = AbiFunction.encodeData(GET_LIMIT_USAGE, [wallet, usageHash])
65-
const getUsageLimitResult = await provider!.request({
85+
return this.readCurrentUsageLimit(wallet, sessionManagerAddress, usageHash, provider)
86+
}
87+
88+
private async getCurrentValueUsageLimit(
89+
wallet: Address.Address,
90+
sessionManagerAddress: Address.Address,
91+
provider: Provider.Provider,
92+
): Promise<UsageLimit> {
93+
const usageHash = Hash.keccak256(
94+
AbiParameters.encode(
95+
[
96+
{ type: 'address', name: 'signer' },
97+
{ type: 'address', name: 'valueTrackingAddress' },
98+
],
99+
[this.address, VALUE_TRACKING_ADDRESS],
100+
),
101+
)
102+
return this.readCurrentUsageLimit(wallet, sessionManagerAddress, usageHash, provider)
103+
}
104+
105+
private async readCurrentUsageLimit(
106+
wallet: Address.Address,
107+
sessionManagerAddress: Address.Address,
108+
usageHash: Hex.Hex,
109+
provider: Provider.Provider,
110+
): Promise<UsageLimit> {
111+
const readData = AbiFunction.encodeData(Constants.GET_LIMIT_USAGE, [wallet, usageHash])
112+
const getUsageLimitResult = await provider.request({
66113
method: 'eth_call',
67114
params: [
68115
{
@@ -71,7 +118,7 @@ export class Explicit implements ExplicitSessionSigner {
71118
},
72119
],
73120
})
74-
const usageAmount = AbiFunction.decodeResult(GET_LIMIT_USAGE, getUsageLimitResult)
121+
const usageAmount = AbiFunction.decodeResult(Constants.GET_LIMIT_USAGE, getUsageLimitResult)
75122
return {
76123
usageHash,
77124
usageAmount,
@@ -99,7 +146,7 @@ export class Explicit implements ExplicitSessionSigner {
99146
let value: Bytes.Bytes = callDataValue.map((b, i) => b & rule.mask[i]!)
100147
if (rule.cumulative) {
101148
if (provider) {
102-
const { usageAmount } = await this.getCurrentUsageLimit(
149+
const { usageAmount } = await this.getCurrentPermissionUsageLimit(
103150
wallet,
104151
sessionManagerAddress,
105152
permission,
@@ -109,7 +156,7 @@ export class Explicit implements ExplicitSessionSigner {
109156
// Increment the value
110157
value = Bytes.fromNumber(usageAmount + Bytes.toBigInt(value), { size: 32 })
111158
} else {
112-
throw new Error('Cumulative rules require a provider and usage hash')
159+
throw new Error('Cumulative rules require a provider')
113160
}
114161
}
115162

@@ -147,8 +194,8 @@ export class Explicit implements ExplicitSessionSigner {
147194
provider?: Provider.Provider,
148195
): Promise<boolean> {
149196
if (
150-
call.data.length > 4 &&
151-
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(INCREMENT_USAGE_LIMIT))
197+
Hex.size(call.data) > 4 &&
198+
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(Constants.INCREMENT_USAGE_LIMIT))
152199
) {
153200
// Can sign increment usage calls
154201
return true
@@ -174,8 +221,8 @@ export class Explicit implements ExplicitSessionSigner {
174221
): Promise<SessionSignature.SessionCallSignature> {
175222
let permissionIndex: number
176223
if (
177-
call.data.length > 4 &&
178-
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(INCREMENT_USAGE_LIMIT))
224+
Hex.size(call.data) > 4 &&
225+
Hex.isEqual(Hex.slice(call.data, 0, 4), AbiFunction.getSelector(Constants.INCREMENT_USAGE_LIMIT))
179226
) {
180227
// Permission check not required. Use the first permission
181228
permissionIndex = 0
@@ -231,12 +278,35 @@ export class Explicit implements ExplicitSessionSigner {
231278
if (Bytes.toBigInt(value) === 0n) continue
232279

233280
// read on-chain "used so far"
234-
const currentUsage = await this.getCurrentUsageLimit(wallet, sessionManagerAddress, perm, ruleIndex, provider)
281+
const currentUsage = await this.getCurrentPermissionUsageLimit(
282+
wallet,
283+
sessionManagerAddress,
284+
perm,
285+
ruleIndex,
286+
provider!,
287+
)
235288
increments.push({
236289
usageHash: currentUsage.usageHash,
237290
usageAmount: Bytes.toBigInt(Bytes.fromNumber(currentUsage.usageAmount + Bytes.toBigInt(value), { size: 32 })),
238291
})
239292
}
293+
294+
// Check the value
295+
if (call.value !== 0n) {
296+
if (!provider) {
297+
throw new Error('Value transaction validation requires a provider')
298+
}
299+
const currentUsage = await this.getCurrentValueUsageLimit(wallet, sessionManagerAddress, provider)
300+
const value = Bytes.fromNumber(currentUsage.usageAmount + call.value, { size: 32 })
301+
if (Bytes.toBigInt(value) > this.sessionPermissions.valueLimit) {
302+
throw new Error('Value transaction validation failed')
303+
}
304+
increments.push({
305+
usageHash: currentUsage.usageHash,
306+
usageAmount: Bytes.toBigInt(value),
307+
})
308+
}
309+
240310
return increments
241311
}
242312
}

packages/wallet/core/test/session-manager.test.ts

Lines changed: 132 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ describe('SessionManager', () => {
281281
const simulateTransaction = async (
282282
provider: Provider.Provider,
283283
transaction: { to: Address.Address; data: Hex.Hex },
284-
expectedEventTopic: Hex.Hex,
284+
expectedEventTopic?: Hex.Hex,
285285
) => {
286286
console.log('Simulating transaction', transaction)
287287
const txHash = await provider.request({
@@ -300,20 +300,21 @@ describe('SessionManager', () => {
300300
throw new Error('Transaction receipt not found')
301301
}
302302

303-
// Check for event
304-
if (!receipt.logs) {
305-
throw new Error('No events emitted')
306-
}
307-
if (!receipt.logs.some((log) => log.topics.includes(expectedEventTopic))) {
308-
throw new Error(`Expected topic ${expectedEventTopic} not found in event`)
303+
if (expectedEventTopic) {
304+
// Check for event
305+
if (!receipt.logs) {
306+
throw new Error('No events emitted')
307+
}
308+
if (!receipt.logs.some((log) => log.topics.includes(expectedEventTopic))) {
309+
throw new Error(`Expected topic ${expectedEventTopic} not found in event`)
310+
}
309311
}
310312

311313
return receipt
312314
}
313315

314-
// Submit a real transaction with a wallet that has a SessionManager using implicit session
315316
it(
316-
'Submits a real transaction with a wallet that has a SessionManager using implicit session',
317+
'signs a payload using an implicit session',
317318
async () => {
318319
// Check the contracts have been deployed
319320
const provider = Provider.from(RpcTransport.fromHttp(LOCAL_RPC_URL))
@@ -384,7 +385,7 @@ describe('SessionManager', () => {
384385
)
385386

386387
it(
387-
'Submits a real transaction with a wallet that has a SessionManager using explicit session',
388+
'signs a payload using an explicit session',
388389
async () => {
389390
const provider = Provider.from(RpcTransport.fromHttp(LOCAL_RPC_URL))
390391
const chainId = BigInt(await provider.request({ method: 'eth_chainId' }))
@@ -450,7 +451,7 @@ describe('SessionManager', () => {
450451
)
451452

452453
it(
453-
'Submits a real transaction with a wallet that has a SessionManager using explicit session using cumulative rule',
454+
'signs a payload using an explicit session',
454455
async () => {
455456
const provider = Provider.from(RpcTransport.fromHttp(LOCAL_RPC_URL))
456457
const chainId = BigInt(await provider.request({ method: 'eth_chainId' }))
@@ -525,12 +526,128 @@ describe('SessionManager', () => {
525526
expect(increment).not.toBeNull()
526527
expect(increment).toBeDefined()
527528

528-
if (increment) {
529-
// Build, sign and send the transaction
530-
const transaction = await buildAndSignCall(wallet, sessionManager, [call, increment], provider, chainId)
531-
await simulateTransaction(provider, transaction, EMITTER_EVENT_TOPICS[0])
529+
if (!increment) {
530+
return
531+
}
532+
533+
// Build, sign and send the transaction
534+
const transaction = await buildAndSignCall(wallet, sessionManager, [call, increment], provider, chainId)
535+
await simulateTransaction(provider, transaction, EMITTER_EVENT_TOPICS[0])
536+
537+
// Repeat call fails because the usage limit has been reached
538+
try {
539+
await sessionManager.prepareIncrement(wallet.address, chainId, [call])
540+
throw new Error('Expected call as no signer supported to fail')
541+
} catch (error) {
542+
expect(error).toBeDefined()
543+
expect(error.message).toContain('No signer supported')
544+
}
545+
},
546+
timeout,
547+
)
548+
549+
it(
550+
'signs a payload sending value using an explicit session',
551+
async () => {
552+
const provider = Provider.from(RpcTransport.fromHttp(LOCAL_RPC_URL))
553+
const chainId = BigInt(await provider.request({ method: 'eth_chainId' }))
554+
555+
// Create explicit signer
556+
const explicitPrivateKey = Secp256k1.randomPrivateKey()
557+
const explicitAddress = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: explicitPrivateKey }))
558+
const sessionPermission: Signers.Session.ExplicitParams = {
559+
valueLimit: 1000000000000000000n, // 1 ETH
560+
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now
561+
permissions: [
562+
{
563+
target: explicitAddress,
564+
rules: [
565+
{
566+
cumulative: true,
567+
operation: Permission.ParameterOperation.EQUAL,
568+
value: Bytes.padRight(Bytes.fromHex(AbiFunction.getSelector(EMITTER_FUNCTIONS[0])), 32),
569+
offset: 0n,
570+
mask: Permission.SELECTOR_MASK,
571+
},
572+
],
573+
},
574+
],
575+
}
576+
const explicitSigner = new Signers.Session.Explicit(explicitPrivateKey, sessionPermission)
577+
// Test manually building the session topology
578+
const sessionTopology = SessionConfig.addExplicitSession(SessionConfig.emptySessionsTopology(identityAddress), {
579+
...sessionPermission,
580+
signer: explicitSigner.address,
581+
})
582+
await stateProvider.saveTree(SessionConfig.sessionsTopologyToConfigurationTree(sessionTopology))
583+
const imageHash = GenericTree.hash(SessionConfig.sessionsTopologyToConfigurationTree(sessionTopology))
584+
585+
// Create the wallet
586+
const wallet = await Wallet.fromConfiguration(
587+
{
588+
threshold: 1n,
589+
checkpoint: 0n,
590+
topology: [
591+
// Random explicit signer will randomise the image hash
592+
{
593+
type: 'sapient-signer',
594+
address: Constants.DefaultSessionManager,
595+
weight: 1n,
596+
imageHash,
597+
},
598+
// Include a random node leaf (bytes32) to prevent image hash collision
599+
Hex.random(32),
600+
],
601+
},
602+
{
603+
stateProvider,
604+
},
605+
)
606+
// Force 1 ETH to the wallet
607+
await provider.request({
608+
method: 'anvil_setBalance',
609+
params: [wallet.address, Hex.fromNumber(1000000000000000000n)],
610+
})
611+
// Create the session manager
612+
const sessionManager = new Signers.SessionManager(wallet, {
613+
provider,
614+
explicitSigners: [explicitSigner],
615+
})
616+
617+
const call: Payload.Call = {
618+
to: explicitAddress,
619+
value: 1000000000000000000n, // 1 ETH
620+
data: AbiFunction.encodeData(EMITTER_FUNCTIONS[0]), // Explicit emit
621+
gasLimit: 0n,
622+
delegateCall: false,
623+
onlyFallback: false,
624+
behaviorOnError: 'revert',
625+
}
626+
627+
const increment = await sessionManager.prepareIncrement(wallet.address, chainId, [call])
628+
expect(increment).not.toBeNull()
629+
expect(increment).toBeDefined()
630+
631+
if (!increment) {
632+
return
532633
}
533634

635+
// Build, sign and send the transaction
636+
const transaction = await buildAndSignCall(wallet, sessionManager, [call, increment], provider, chainId)
637+
await simulateTransaction(provider, transaction)
638+
639+
// Check the balances
640+
const walletBalance = await provider.request({
641+
method: 'eth_getBalance',
642+
params: [wallet.address, 'latest'],
643+
})
644+
expect(BigInt(walletBalance)).toBe(0n)
645+
const explicitAddressBalance = await provider.request({
646+
method: 'eth_getBalance',
647+
params: [explicitAddress, 'latest'],
648+
})
649+
expect(BigInt(explicitAddressBalance)).toBe(1000000000000000000n)
650+
534651
// Repeat call fails because the usage limit has been reached
535652
try {
536653
await sessionManager.prepareIncrement(wallet.address, chainId, [call])

0 commit comments

Comments
 (0)