@@ -35,6 +35,8 @@ type EVMFacilitator struct {
3535 address common.Address
3636}
3737
38+ const eip3009DeadlineBuffer int64 = 6
39+
3840func NewEVMFacilitator (network string , url string , privateKeyHex string ) (* EVMFacilitator , error ) {
3941 if network == "" && url == "" {
4042 return nil , fmt .Errorf ("network or rpc url must be provided" )
@@ -161,6 +163,13 @@ func (t *EVMFacilitator) verifyWithEndpointFallback(ctx context.Context, operati
161163
162164// Verify detects the payload type and routes to the appropriate verification method.
163165func (t * EVMFacilitator ) Verify (ctx context.Context , payload * types.PaymentPayload , req * types.PaymentRequirements ) (* types.PaymentVerifyResponse , error ) {
166+ if payload == nil || req == nil {
167+ return & types.PaymentVerifyResponse {
168+ IsValid : false ,
169+ InvalidReason : types .ErrInvalidPayloadFormat .Error (),
170+ }, nil
171+ }
172+
164173 raw , err := json .Marshal (payload .Payload )
165174 if err != nil {
166175 return & types.PaymentVerifyResponse {
@@ -180,6 +189,18 @@ func (t *EVMFacilitator) Verify(ctx context.Context, payload *types.PaymentPaylo
180189
181190// Settle detects the payload type and routes to the appropriate settlement method.
182191func (t * EVMFacilitator ) Settle (ctx context.Context , payload * types.PaymentPayload , req * types.PaymentRequirements ) (* types.PaymentSettleResponse , error ) {
192+ network := types .Network ("" )
193+ if req != nil {
194+ network = types .Network (req .Network )
195+ }
196+ if payload == nil || req == nil {
197+ return & types.PaymentSettleResponse {
198+ Success : false ,
199+ ErrorReason : types .ErrInvalidPayloadFormat .Error (),
200+ Network : network ,
201+ }, nil
202+ }
203+
183204 raw , err := json .Marshal (payload .Payload )
184205 if err != nil {
185206 return & types.PaymentSettleResponse {
@@ -193,6 +214,116 @@ func (t *EVMFacilitator) Settle(ctx context.Context, payload *types.PaymentPaylo
193214 return t .settleEIP3009 (ctx , payload , req , raw )
194215}
195216
217+ func (t * EVMFacilitator ) validateEVMPaymentEnvelope (payload * types.PaymentPayload , req * types.PaymentRequirements , payer string ) * types.PaymentVerifyResponse {
218+ if payload == nil || req == nil {
219+ return & types.PaymentVerifyResponse {
220+ IsValid : false ,
221+ InvalidReason : types .ErrInvalidPayloadFormat .Error (),
222+ Payer : payer ,
223+ }
224+ }
225+ if payload .Accepted .Scheme != string (t .scheme ) || req .Scheme != string (t .scheme ) {
226+ return & types.PaymentVerifyResponse {
227+ IsValid : false ,
228+ InvalidReason : types .ErrIncompatibleScheme .Error (),
229+ Payer : payer ,
230+ }
231+ }
232+ if payload .Accepted .Network != t .network || req .Network != t .network {
233+ return & types.PaymentVerifyResponse {
234+ IsValid : false ,
235+ InvalidReason : types .ErrNetworkMismatch .Error (),
236+ Payer : payer ,
237+ }
238+ }
239+ if ! strings .EqualFold (strings .TrimSpace (payload .Accepted .Asset ), strings .TrimSpace (req .Asset )) {
240+ return & types.PaymentVerifyResponse {
241+ IsValid : false ,
242+ InvalidReason : types .ErrTokenMismatch .Error (),
243+ Payer : payer ,
244+ }
245+ }
246+ if payload .Accepted .Amount != req .Amount {
247+ return & types.PaymentVerifyResponse {
248+ IsValid : false ,
249+ InvalidReason : types .ErrAmountMismatch .Error (),
250+ Payer : payer ,
251+ }
252+ }
253+ if ! evmAddressMatches (payload .Accepted .PayTo , req .PayTo ) {
254+ return & types.PaymentVerifyResponse {
255+ IsValid : false ,
256+ InvalidReason : types .ErrRecipientMismatch .Error (),
257+ Payer : payer ,
258+ }
259+ }
260+ return nil
261+ }
262+
263+ func validateEIP3009Authorization (auth * evm.Authorization , req * types.PaymentRequirements , payer string ) * types.PaymentVerifyResponse {
264+ if auth == nil || auth .Value == nil || auth .ValidAfter == nil || auth .ValidBefore == nil || req == nil {
265+ return & types.PaymentVerifyResponse {
266+ IsValid : false ,
267+ InvalidReason : types .ErrInvalidPayloadFormat .Error (),
268+ Payer : payer ,
269+ }
270+ }
271+
272+ if ! common .IsHexAddress (strings .TrimSpace (req .PayTo )) || auth .To != common .HexToAddress (strings .TrimSpace (req .PayTo )) {
273+ return & types.PaymentVerifyResponse {
274+ IsValid : false ,
275+ InvalidReason : types .ErrRecipientMismatch .Error (),
276+ Payer : payer ,
277+ }
278+ }
279+
280+ reqAmount , ok := new (big.Int ).SetString (req .Amount , 10 )
281+ if ! ok || reqAmount .Sign () <= 0 || auth .Value .Cmp (reqAmount ) != 0 {
282+ return & types.PaymentVerifyResponse {
283+ IsValid : false ,
284+ InvalidReason : types .ErrAmountMismatch .Error (),
285+ Payer : payer ,
286+ }
287+ }
288+
289+ now := time .Now ().Unix ()
290+ if auth .ValidBefore .Cmp (big .NewInt (now + eip3009DeadlineBuffer )) < 0 {
291+ return & types.PaymentVerifyResponse {
292+ IsValid : false ,
293+ InvalidReason : types .ErrAuthorizationExpired .Error (),
294+ Payer : payer ,
295+ }
296+ }
297+ if auth .ValidAfter .Cmp (big .NewInt (now )) > 0 {
298+ return & types.PaymentVerifyResponse {
299+ IsValid : false ,
300+ InvalidReason : types .ErrAuthorizationNotYetValid .Error (),
301+ Payer : payer ,
302+ }
303+ }
304+
305+ return nil
306+ }
307+
308+ func evmAddressMatches (left string , right string ) bool {
309+ left = strings .TrimSpace (left )
310+ right = strings .TrimSpace (right )
311+ if ! common .IsHexAddress (left ) || ! common .IsHexAddress (right ) {
312+ return false
313+ }
314+ return common .HexToAddress (left ) == common .HexToAddress (right )
315+ }
316+
317+ func evmSettleResponseFromInvalid (invalid * types.PaymentVerifyResponse , network types.Network ) * types.PaymentSettleResponse {
318+ return & types.PaymentSettleResponse {
319+ Success : false ,
320+ ErrorReason : invalid .InvalidReason ,
321+ ErrorMessage : invalid .InvalidMessage ,
322+ Payer : invalid .Payer ,
323+ Network : network ,
324+ }
325+ }
326+
196327func (t * EVMFacilitator ) Supported () * types.SupportedResponse {
197328 return & types.SupportedResponse {
198329 Kinds : []types.SupportedKind {{
@@ -233,45 +364,39 @@ func (t *EVMFacilitator) verifyEIP3009(ctx context.Context, payload *types.Payme
233364 InvalidReason : types .ErrInvalidPayloadFormat .Error (),
234365 }, nil
235366 }
367+ auth := evmPayload .Authorization
368+ payer := auth .From .String ()
236369
237- // Step 2: Scheme verification (scheme lives inside payload.Accepted in v2).
238- if payload .Accepted .Scheme != string (t .scheme ) || req .Scheme != string (t .scheme ) {
239- return & types.PaymentVerifyResponse {
240- IsValid : false ,
241- InvalidReason : types .ErrIncompatibleScheme .Error (),
242- Payer : evmPayload .Authorization .From .String (),
243- }, nil
370+ // Step 2: Bind client-echoed requirements to the server requirements.
371+ if invalid := t .validateEVMPaymentEnvelope (payload , req , payer ); invalid != nil {
372+ return invalid , nil
373+ }
374+ if invalid := validateEIP3009Authorization (auth , req , payer ); invalid != nil {
375+ return invalid , nil
244376 }
245377
246378 // Step 3: Network info and Contract info
247- if payload .Accepted .Network != t .network {
248- return & types.PaymentVerifyResponse {
249- IsValid : false ,
250- InvalidReason : types .ErrNetworkMismatch .Error (),
251- Payer : evmPayload .Authorization .From .String (),
252- }, nil
253- }
254- chainID := evm .GetChainID (payload .Accepted .Network )
379+ chainID := evm .GetChainID (req .Network )
255380 if chainID == nil {
256381 return & types.PaymentVerifyResponse {
257382 IsValid : false ,
258383 InvalidReason : types .ErrInvalidNetwork .Error (),
259- Payer : evmPayload . Authorization . From . String () ,
384+ Payer : payer ,
260385 }, nil
261386 }
262387 if chainID .Cmp (t .networkID ) != 0 {
263388 return & types.PaymentVerifyResponse {
264389 IsValid : false ,
265390 InvalidReason : types .ErrNetworkIDMismatch .Error (),
266- Payer : evmPayload . Authorization . From . String () ,
391+ Payer : payer ,
267392 }, nil
268393 }
269- domainConfig := evm .GetDomainConfig (payload . Accepted .Network , req .Asset )
394+ domainConfig := evm .GetDomainConfig (req .Network , req .Asset )
270395 if domainConfig == nil {
271396 return & types.PaymentVerifyResponse {
272397 IsValid : false ,
273398 InvalidReason : types .ErrTokenMismatch .Error (),
274- Payer : evmPayload . Authorization . From . String () ,
399+ Payer : payer ,
275400 }, nil
276401 }
277402
@@ -280,7 +405,7 @@ func (t *EVMFacilitator) verifyEIP3009(ctx context.Context, payload *types.Payme
280405 if err != nil {
281406 return nil , err
282407 }
283- digest := evm .HashEip3009 (evmPayload . Authorization , domainConfig )
408+ digest := evm .HashEip3009 (auth , domainConfig )
284409 pubkey , err := evm .Ecrecover (digest , sig )
285410 if err != nil {
286411 return nil , err
@@ -289,50 +414,55 @@ func (t *EVMFacilitator) verifyEIP3009(ctx context.Context, payload *types.Payme
289414 return & types.PaymentVerifyResponse {
290415 IsValid : false ,
291416 InvalidReason : types .ErrInvalidSignature .Error (),
292- Payer : evmPayload . Authorization . From . String () ,
417+ Payer : payer ,
293418 }, nil
294419 }
295- if evm .PubkeyToAddress (pubkey ) != evmPayload . Authorization .From {
420+ if evm .PubkeyToAddress (pubkey ) != auth .From {
296421 return & types.PaymentVerifyResponse {
297422 IsValid : false ,
298423 InvalidReason : types .ErrInvalidSignature .Error (),
299- Payer : evmPayload . Authorization . From . String () ,
424+ Payer : payer ,
300425 }, nil
301426 }
302427
303- // Step 5: Validate payTo
304-
305- // Step 6: Deadline check
306-
307- // Step 7: TODO: Nonce freshness check (optional in v1)
308-
309- // Step 8: Check ERC20 balance
428+ // Step 5: Check nonce freshness
310429 contract , err := eip3009 .NewEip3009 (domainConfig .VerifyingContract , client )
311430 if err != nil {
312431 return nil , fmt .Errorf ("contract bind failed: %w" , err )
313432 }
314- balance , err := contract .BalanceOf (& bind.CallOpts {Context : ctx }, evmPayload .Authorization .From )
433+ used , err := contract .AuthorizationState (& bind.CallOpts {Context : ctx }, auth .From , auth .Nonce )
434+ if err != nil {
435+ return nil , fmt .Errorf ("failed to get authorization state: %w" , err )
436+ }
437+ if used {
438+ return & types.PaymentVerifyResponse {
439+ IsValid : false ,
440+ InvalidReason : types .ErrAuthorizationAlreadyUsed .Error (),
441+ Payer : payer ,
442+ }, nil
443+ }
444+
445+ // Step 6: Check ERC20 balance
446+ balance , err := contract .BalanceOf (& bind.CallOpts {Context : ctx }, auth .From )
315447 if err != nil {
316448 return nil , fmt .Errorf ("failed to get balance: %w" , err )
317449 }
318- if balance .Cmp (evmPayload . Authorization .Value ) < 0 {
450+ if balance .Cmp (auth .Value ) < 0 {
319451 return & types.PaymentVerifyResponse {
320452 IsValid : false ,
321453 InvalidReason : types .ErrInsufficientBalance .Error (),
322- Payer : evmPayload . Authorization . From . String () ,
454+ Payer : payer ,
323455 }, nil
324456 }
325457
326- // Step 9: Check value in permit matches requirement
327-
328- // Step 10: TODO: Check minimum payment threshold (e.g. for gas overhead)
458+ // Step 7: TODO: Check minimum payment threshold (e.g. for gas overhead)
329459
330- // Step 11 : TODO: Check if resource already paid (next version)
460+ // Step 8 : TODO: Check if resource already paid (next version)
331461
332462 // ✅ All checks passed
333463 return & types.PaymentVerifyResponse {
334464 IsValid : true ,
335- Payer : evmPayload . Authorization . From . String () ,
465+ Payer : payer ,
336466 }, nil
337467}
338468
@@ -356,6 +486,13 @@ func (t *EVMFacilitator) settleEIP3009(ctx context.Context, payload *types.Payme
356486 }
357487 payer := evmPayload .Authorization .From .String ()
358488
489+ if invalid := t .validateEVMPaymentEnvelope (payload , req , payer ); invalid != nil {
490+ return evmSettleResponseFromInvalid (invalid , network ), nil
491+ }
492+ if invalid := validateEIP3009Authorization (evmPayload .Authorization , req , payer ); invalid != nil {
493+ return evmSettleResponseFromInvalid (invalid , network ), nil
494+ }
495+
359496 networkID := evm .GetChainID (req .Network )
360497 if networkID == nil {
361498 return & types.PaymentSettleResponse {
@@ -397,6 +534,24 @@ func (t *EVMFacilitator) settleEIP3009(ctx context.Context, payload *types.Payme
397534 Network : network ,
398535 }, nil
399536 }
537+ used , err := contract .AuthorizationState (& bind.CallOpts {Context : ctx }, evmPayload .Authorization .From , evmPayload .Authorization .Nonce )
538+ if err != nil {
539+ return & types.PaymentSettleResponse {
540+ Success : false ,
541+ ErrorReason : types .ErrTransactionFailed .Error (),
542+ ErrorMessage : err .Error (),
543+ Payer : payer ,
544+ Network : network ,
545+ }, nil
546+ }
547+ if used {
548+ return & types.PaymentSettleResponse {
549+ Success : false ,
550+ ErrorReason : types .ErrAuthorizationAlreadyUsed .Error (),
551+ Payer : payer ,
552+ Network : network ,
553+ }, nil
554+ }
400555 clientSig , err := evm .ParseSignature (evmPayload .Signature ) // client signature
401556 if err != nil {
402557 return & types.PaymentSettleResponse {
0 commit comments