88 AccountService ,
99 centrifugeIdFromAssetId ,
1010 VaultService ,
11+ AssetService ,
1112} from "../services" ;
1213import { VaultCrosschainInProgressTypes } from "ponder:schema" ;
1314
@@ -132,6 +133,33 @@ multiMapper("hub:UpdateVault", async ({ event, context }) => {
132133 await vault . setCrosschainInProgress ( vaultUpdateKind ) . save ( event ) ;
133134} ) ;
134135
136+ multiMapper ( "hub:UpdateContract" , async ( { event, context } ) => {
137+ logEvent ( event , context , "hub:UpdateContract" ) ;
138+ const { centrifugeId : destCentrifugeId , poolId, scId : tokenId , payload } = event . args ;
139+
140+ const decoded = decodeUpdateContract ( payload ) ;
141+ if ( ! decoded || ! decoded . payload )
142+ return serviceError ( `Invalid update contract payload: ${ payload } ` ) ;
143+
144+ if ( decoded . kind === "MaxReserve" && "maxReserve" in decoded . payload ) {
145+ const { assetId, maxReserve } = decoded . payload as { assetId : bigint ; maxReserve : bigint } ;
146+ const asset = await AssetService . get ( context , { id : assetId } ) ;
147+ if ( ! asset )
148+ return serviceError ( `Asset not found for assetId ${ assetId } . Cannot update vault maxReserve` ) ;
149+ const assetAddress = asset . read ( ) . address as `0x${string } `;
150+ if ( ! assetAddress ) return serviceError ( `Asset has no address for assetId ${ assetId } ` ) ;
151+
152+ const vault = ( await VaultService . get ( context , {
153+ centrifugeId : destCentrifugeId . toString ( ) ,
154+ poolId,
155+ tokenId,
156+ assetAddress,
157+ } ) ) as VaultService | null ;
158+ if ( ! vault ) return serviceError ( `Vault not found. Cannot update maxReserve` ) ;
159+ await vault . setMaxReserve ( maxReserve ) . setCrosschainInProgress ( ) . save ( event ) ;
160+ }
161+ } ) ;
162+
135163enum RestrictionType {
136164 "Invalid" ,
137165 "Member" ,
@@ -148,7 +176,7 @@ function decodeUpdateRestriction(
148176 payload : `0x${string } `
149177) :
150178 | [
151- restrictionType : RestrictionType . Member | RestrictionType . Freeze | RestrictionType . Unfreeze ,
179+ restrictionType : ( typeof RestrictionType ) [ keyof typeof RestrictionType ] ,
152180 accountAddress : `0x${string } `,
153181 validUntil : Date | null ,
154182 ]
@@ -172,3 +200,153 @@ function decodeUpdateRestriction(
172200 return null ;
173201 }
174202}
203+
204+ // ---------------------------------------------------------------------------
205+ // UpdateContract: single-pass decoding (SyncManager, OnOfframpManager, BaseTransferHook)
206+ // ---------------------------------------------------------------------------
207+
208+ /** ABI word size. Solidity abi.encode uses 32-byte words; uint8/uint128/bool are right-aligned. */
209+ const WORD_SIZE = 32 ;
210+ /** Start offset of word N (0-based). */
211+ const word = ( n : number ) => n * WORD_SIZE ;
212+ /** Decode a value from the 32-byte ABI word at wordIndex using the given decoder. */
213+ function decodeAtWord < T > ( buffer : Buffer , wordIndex : number , decoder : ( chunk : Buffer ) => T ) : T {
214+ const start = wordIndex * WORD_SIZE ;
215+ return decoder ( buffer . subarray ( start , start + WORD_SIZE ) ) ;
216+ }
217+ /** Decoder: right-aligned uint8 in a 32-byte word (bytes 31). */
218+ const decodeUint8InWord = ( chunk : Buffer ) : number => chunk . readUInt8 ( WORD_SIZE - 1 ) ;
219+ /** Decoder: right-aligned uint128 in a 32-byte word (bytes 16–31). */
220+ const decodeUint128InWord = ( chunk : Buffer ) : bigint =>
221+ chunk . readBigUInt64BE ( 16 ) | ( chunk . readBigUInt64BE ( 24 ) << 64n ) ;
222+ /** Decoder: bytes32 as 20-byte address (right-padded, bytes 12–31). */
223+ const decodeBytes32Address = ( chunk : Buffer ) : `0x${string } ` =>
224+ `0x${ chunk . subarray ( 12 , 32 ) . toString ( "hex" ) } ` as `0x${string } `;
225+ /** Payload kind (uint8) in UpdateContract; matches SyncManager / OnOfframpManager / BaseTransferHook TrustedCall. */
226+ enum UpdateContractPayloadKind {
227+ "Valuation" , // SyncManager.Valuation | OnOfframpManager.Onramp | BaseTransferHook.UpdateHookManager
228+ "MaxReserve" , // SyncManager.MaxReserve | OnOfframpManager.Relayer
229+ "Offramp" , // OnOfframpManager.Offramp
230+ "Withdraw" , // OnOfframpManager.Withdraw
231+ }
232+
233+ /** All possible decoded payloads from protocol UpdateContract, by kind. Payload is null when shape is unrecognized. */
234+ export type DecodedUpdateContract =
235+ | {
236+ kind : "Valuation" ;
237+ payload :
238+ | { valuation : `0x${string } ` } // SyncManager
239+ | { assetId : bigint ; isEnabled : boolean } // OnOfframpManager.Onramp
240+ | { manager : `0x${string } `; canManage : boolean } // BaseTransferHook.UpdateHookManager
241+ | null ;
242+ }
243+ | {
244+ kind : "MaxReserve" ;
245+ payload :
246+ | { assetId : bigint ; maxReserve : bigint } // SyncManager
247+ | { relayerAddress : `0x${string } `; isEnabled : boolean } // OnOfframpManager.Relayer
248+ | null ;
249+ }
250+ | {
251+ kind : "Offramp" ;
252+ payload :
253+ | { assetId : bigint ; receiverAddress : `0x${string } `; isEnabled : boolean } // OnOfframpManager.Offramp
254+ | null ;
255+ }
256+ | {
257+ kind : "Withdraw" ;
258+ payload :
259+ | { assetId : bigint ; amount : bigint ; receiverAddress : `0x${string } ` } // OnOfframpManager.Withdraw
260+ | null ;
261+ } ;
262+
263+ /** True if word at index looks like a right-aligned uint128 (bytes 0–15 zero). */
264+ function isWordZeroPaddedUint128 ( b : Buffer , wordIndex : number ) : boolean {
265+ const start = wordIndex * WORD_SIZE ;
266+ for ( let i = 0 ; i < 16 ; i ++ ) if ( b [ start + i ] !== 0 ) return false ;
267+ return true ;
268+ }
269+
270+ /**
271+ * Decodes the update contract payload into its parameters.
272+ * Does not resolve target address; decoding is based only on payload kind and shape.
273+ * @param payload - The payload to decode.
274+ * @returns The decoded parameters.
275+ */
276+ export function decodeUpdateContract ( payload : `0x${string } `) : DecodedUpdateContract | null {
277+ const b = Buffer . from ( payload . slice ( 2 ) , "hex" ) ;
278+ if ( b . length < WORD_SIZE ) return null ;
279+ const kindValue = decodeAtWord ( b , 0 , decodeUint8InWord ) ;
280+ const kind = Object . keys ( UpdateContractPayloadKind ) [ kindValue ] as
281+ | keyof typeof UpdateContractPayloadKind
282+ | undefined ;
283+ if ( kind === undefined ) return null ;
284+
285+ const result : DecodedUpdateContract = {
286+ kind,
287+ payload : null ,
288+ } ;
289+
290+ // ABI layout: each slot is 32 bytes; uint8/uint128/bool right-aligned.
291+ switch ( kind ) {
292+ case "Valuation" :
293+ // (uint8, bytes32) = 2 words — SyncManager only
294+ if ( b . length === word ( 2 ) ) {
295+ result . payload = {
296+ valuation : decodeAtWord ( b , 1 , decodeBytes32Address ) ,
297+ } ;
298+ }
299+ // (uint8, uint128, bool) = 3 words (Onramp) | (uint8, bytes32, bool) = 3 words (UpdateHookManager)
300+ if ( b . length === word ( 3 ) && isWordZeroPaddedUint128 ( b , 1 ) ) {
301+ result . payload = {
302+ assetId : decodeAtWord ( b , 1 , decodeUint128InWord ) ,
303+ isEnabled : decodeAtWord ( b , 2 , decodeUint8InWord ) !== 0 ,
304+ } ;
305+ }
306+ if ( b . length === word ( 3 ) && ! isWordZeroPaddedUint128 ( b , 1 ) ) {
307+ result . payload = {
308+ manager : decodeAtWord ( b , 1 , decodeBytes32Address ) ,
309+ canManage : decodeAtWord ( b , 2 , decodeUint8InWord ) !== 0 ,
310+ } ;
311+ }
312+ break ;
313+ case "MaxReserve" :
314+ // (uint8, uint128, uint128) = 3 words (SyncManager) | (uint8, bytes32, bool) = 3 words (Relayer)
315+ if ( b . length === word ( 3 ) ) {
316+ if ( isWordZeroPaddedUint128 ( b , 1 ) && isWordZeroPaddedUint128 ( b , 2 ) ) {
317+ result . payload = {
318+ assetId : decodeAtWord ( b , 1 , decodeUint128InWord ) ,
319+ maxReserve : decodeAtWord ( b , 2 , decodeUint128InWord ) ,
320+ } ;
321+ } else {
322+ result . payload = {
323+ relayerAddress : decodeAtWord ( b , 1 , decodeBytes32Address ) ,
324+ isEnabled : decodeAtWord ( b , 2 , decodeUint8InWord ) !== 0 ,
325+ } ;
326+ }
327+ }
328+ break ;
329+ case "Offramp" :
330+ // (uint8, uint128, bytes32, bool) = 4 words
331+ if ( b . length === word ( 4 ) ) {
332+ result . payload = {
333+ assetId : decodeAtWord ( b , 1 , decodeUint128InWord ) ,
334+ receiverAddress : decodeAtWord ( b , 2 , decodeBytes32Address ) ,
335+ isEnabled : decodeAtWord ( b , 3 , decodeUint8InWord ) !== 0 ,
336+ } ;
337+ }
338+ break ;
339+ case "Withdraw" :
340+ // (uint8, uint128, uint128, bytes32) = 4 words
341+ if ( b . length === word ( 4 ) ) {
342+ result . payload = {
343+ assetId : decodeAtWord ( b , 1 , decodeUint128InWord ) ,
344+ amount : decodeAtWord ( b , 2 , decodeUint128InWord ) ,
345+ receiverAddress : decodeAtWord ( b , 3 , decodeBytes32Address ) ,
346+ } ;
347+ }
348+ break ;
349+ }
350+
351+ return result ;
352+ }
0 commit comments