Skip to content

Commit b51a683

Browse files
committed
ensure from and nonce = 0 for AssignDeposit intentions
1 parent b9ae69e commit b51a683

File tree

3 files changed

+24
-69
lines changed

3 files changed

+24
-69
lines changed

src/proposer.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
getVaultsForController,
3737
updateVaultControllers,
3838
} from './utils/vaults.js'
39-
import { PROPOSER_VAULT_ID, SEED_CONFIG } from './config/seedingConfig.js'
39+
import { SEED_CONFIG } from './config/seedingConfig.js'
4040
import {
4141
validateIntention,
4242
validateAddress,
@@ -364,6 +364,7 @@ async function getLatestNonce(): Promise<number> {
364364
* Retrieves the latest nonce for a specific vault from the database.
365365
* Returns 0 if no nonce is found for the vault.
366366
*/
367+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
367368
async function getVaultNonce(vaultId: number | string): Promise<number> {
368369
const result = await pool.query('SELECT nonce FROM vaults WHERE vault = $1', [
369370
String(vaultId),
@@ -587,7 +588,7 @@ async function saveBundleData(
587588
continue
588589
}
589590

590-
// Skip nonce updates if from is 0 (edge case safety)
591+
// Skip nonce updates for protocol-level actions (from=0, e.g., AssignDeposit)
591592
if (execution.from === 0) {
592593
continue
593594
}
@@ -921,17 +922,13 @@ async function createAndSubmitSeedingIntention(
921922
return
922923
}
923924

924-
const submitterVaultId = PROPOSER_VAULT_ID.value
925-
const currentNonce = await getVaultNonce(submitterVaultId)
926-
const nextNonce = currentNonce + 1
927-
928925
// Build token summary for logging
929926
const tokenSummary = SEED_CONFIG.map(
930927
(token) => `${token.amount} ${token.symbol || token.address}`
931928
).join(', ')
932929

933930
logger.info(
934-
`Seeding requested for vault ${newVaultId}: controller=${PROPOSER_ADDRESS}, submitterVaultId=${submitterVaultId}, nonce=${nextNonce}, tokens=[${tokenSummary}]`
931+
`Seeding requested for vault ${newVaultId}: controller=${PROPOSER_ADDRESS}, protocol-level AssignDeposit (nonce=0, from=0), tokens=[${tokenSummary}]`
935932
)
936933

937934
const inputs: IntentionInput[] = []
@@ -957,7 +954,7 @@ async function createAndSubmitSeedingIntention(
957954

958955
const intention: Intention = {
959956
action: 'AssignDeposit',
960-
nonce: nextNonce,
957+
nonce: 0, // Protocol-level action: nonce=0 (protocol vault)
961958
expiry: Math.floor(Date.now() / 1000) + 300, // 5 minute expiry
962959
inputs,
963960
outputs,
@@ -974,7 +971,7 @@ async function createAndSubmitSeedingIntention(
974971
await handleIntention(intention, signature, PROPOSER_ADDRESS)
975972

976973
logger.info(
977-
`Successfully submitted AssignDeposit seeding intention for vault ${newVaultId} (nonce: ${nextNonce}, submitter vault: ${submitterVaultId}).`
974+
`Successfully submitted AssignDeposit seeding intention for vault ${newVaultId} (protocol-level: nonce=0, from=0).`
978975
)
979976
}
980977

@@ -1083,7 +1080,6 @@ async function handleIntention(
10831080
discoverAndIngestEthDeposits,
10841081
findDepositWithSufficientRemaining,
10851082
validateVaultIdOnChain,
1086-
getVaultsForController,
10871083
logger,
10881084
diagnostic,
10891085
},

src/utils/intentionHandlers/AssignDeposit.ts

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
* - Discovers deposits from on-chain events (ERC20 or ETH)
88
* - Selects deposits with sufficient remaining balance
99
* - Supports partial deposit assignments (can combine multiple deposits)
10-
* - Determines submitter vault for nonce tracking:
11-
* - If inputs have `from` field: uses that vault ID
12-
* - If no `from` field: queries vaults controlled by the controller
13-
* - If no vaults found: uses from=0 (no nonce update)
14-
* - Sets execution.from to the submitter vault ID for proper nonce tracking
10+
* - AssignDeposit is a protocol-level action: always sets execution.from = 0 (protocol vault)
11+
* - Nonces are not relevant for AssignDeposit; conflicts resolved by bundle inclusion order
1512
*
1613
* At publish time, deposits are assigned and balances are credited to destination vaults.
1714
* If a selected deposit is exhausted, the system automatically falls back to combining
@@ -45,7 +42,6 @@ type AssignDepositContext = {
4542
minAmount: string
4643
}) => Promise<{ id: number; remaining: string } | null>
4744
validateVaultIdOnChain: (vaultId: number) => Promise<void>
48-
getVaultsForController: (controller: string) => Promise<string[]>
4945
logger: { info: (...args: unknown[]) => void }
5046
diagnostic: { info: (...args: unknown[]) => void }
5147
}
@@ -60,41 +56,9 @@ export async function handleAssignDeposit(params: {
6056

6157
await context.validateAssignDepositStructure(intention)
6258

63-
// Determine submitter vault for nonce tracking
64-
// 1. If inputs have `from` field, use that (all inputs must have the same `from` value per validator)
65-
// 2. If no `from` field, determine from controller by querying vaults
66-
let submitterVaultId: number | 0 = 0
67-
const inputsWithFrom = intention.inputs.filter(
68-
(input) => input.from !== undefined
69-
)
70-
if (inputsWithFrom.length > 0) {
71-
// All inputs should have the same `from` value per validator, but double-check
72-
const fromValues = new Set(inputsWithFrom.map((input) => input.from))
73-
if (fromValues.size > 1) {
74-
throw new Error(
75-
'AssignDeposit requires all inputs to have the same `from` vault ID'
76-
)
77-
}
78-
submitterVaultId = inputsWithFrom[0].from as number
79-
} else {
80-
// No `from` field in inputs, determine from controller
81-
const vaults = await context.getVaultsForController(validatedController)
82-
if (vaults.length === 1) {
83-
submitterVaultId = parseInt(vaults[0])
84-
} else if (vaults.length > 1) {
85-
// Multiple vaults controlled by this controller - use the first one
86-
context.logger.info(
87-
`Controller ${validatedController} controls multiple vaults, using first vault ${vaults[0]} for nonce tracking`
88-
)
89-
submitterVaultId = parseInt(vaults[0])
90-
} else {
91-
// No vaults found - cannot determine submitter vault, use 0 (no nonce update)
92-
context.logger.info(
93-
`Controller ${validatedController} does not control any vaults, using from=0 (no nonce update)`
94-
)
95-
submitterVaultId = 0
96-
}
97-
}
59+
// AssignDeposit is a protocol-level action: always use from=0 (protocol vault)
60+
// Nonces are not relevant for AssignDeposit; conflicts resolved by bundle inclusion order
61+
const PROTOCOL_VAULT_ID = 0
9862

9963
const zeroAddress = '0x0000000000000000000000000000000000000000'
10064
const proof: unknown[] = []
@@ -156,17 +120,17 @@ export async function handleAssignDeposit(params: {
156120
context.diagnostic.info('AssignDeposit intention processed', {
157121
controller: validatedController,
158122
count: intention.inputs.length,
159-
submitterVaultId,
123+
protocolVault: PROTOCOL_VAULT_ID,
160124
})
161125
context.logger.info(
162-
`AssignDeposit cached with proof count: ${proof.length}, submitter vault: ${submitterVaultId}`
126+
`AssignDeposit cached with proof count: ${proof.length}, protocol-level action (from=0)`
163127
)
164128

165129
return {
166130
execution: [
167131
{
168132
intention,
169-
from: submitterVaultId,
133+
from: PROTOCOL_VAULT_ID,
170134
proof,
171135
signature: validatedSignature,
172136
},

test/integration/seeding.assignDeposit.test.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ describe('AssignDeposit seeding flow (DB)', () => {
271271
expect(errorThrown).toBe(true)
272272
})
273273

274-
test('Nonce tracking: AssignDeposit updates submitter vault nonce at publish', async () => {
274+
test('Nonce tracking: AssignDeposit does not update vault nonces (protocol-level action)', async () => {
275275
// Create deposit
276276
await insertDepositIfMissing({
277277
tx_hash: TEST_TX,
@@ -290,12 +290,10 @@ describe('AssignDeposit seeding flow (DB)', () => {
290290
'SELECT nonce FROM vaults WHERE vault = $1',
291291
[String(PROPOSER_VAULT_ID)]
292292
)
293-
expect(initialNonce.rows[0].nonce).toBe(0)
293+
const initialNonceValue = initialNonce.rows[0].nonce
294294

295-
// Simulate AssignDeposit intention with nonce = currentNonce + 1
296-
const intentionNonce = initialNonce.rows[0].nonce + 1
297-
298-
// Simulate assignment and nonce update at publish
295+
// AssignDeposit is protocol-level: intention has nonce=0 and from=0
296+
// Simulate assignment (deposit assignment happens, but nonce should not be updated)
299297
const deposit = await findNextDepositWithAnyRemaining({
300298
depositor: PROPOSER_CONTROLLER,
301299
token: TOKEN,
@@ -308,19 +306,16 @@ describe('AssignDeposit seeding flow (DB)', () => {
308306
String(NEW_VAULT_ID)
309307
)
310308

311-
// Update nonce (simulating publishBundle behavior)
312-
await pool.query('UPDATE vaults SET nonce = $2 WHERE vault = $1', [
313-
String(PROPOSER_VAULT_ID),
314-
intentionNonce,
315-
])
309+
// AssignDeposit with from=0 should NOT update any vault nonce
310+
// (saveBundleData skips nonce updates when execution.from === 0)
316311

317-
// Verify nonce was updated
318-
const updatedNonce = await pool.query(
312+
// Verify nonce was NOT updated (should remain unchanged)
313+
const finalNonce = await pool.query(
319314
'SELECT nonce FROM vaults WHERE vault = $1',
320315
[String(PROPOSER_VAULT_ID)]
321316
)
322-
expect(updatedNonce.rows[0].nonce).toBe(intentionNonce)
323-
expect(updatedNonce.rows[0].nonce).toBe(1)
317+
expect(finalNonce.rows[0].nonce).toBe(initialNonceValue)
318+
expect(finalNonce.rows[0].nonce).toBe(0)
324319
})
325320

326321
test('findNextDepositWithAnyRemaining: Returns oldest deposit first', async () => {

0 commit comments

Comments
 (0)