Skip to content

feat: added contracts/evm for evm permit2 update#915

Closed
CarsonRoscoe wants to merge 22 commits intomainfrom
feat/exact-evm-permit-2
Closed

feat: added contracts/evm for evm permit2 update#915
CarsonRoscoe wants to merge 22 commits intomainfrom
feat/exact-evm-permit-2

Conversation

@CarsonRoscoe
Copy link
Contributor

@CarsonRoscoe CarsonRoscoe commented Jan 7, 2026

THE CONTRACTS IN THIS PR HAVE YET TO BE AUDITED. ONLY DEPLOY TO TEST NETWORKS AT THIS TIME

Description

In order to implement #769, an x402Permit2Proxy contract must be written and deployed.

This PR adds a foundry project under contracts/evm, which both acts as a foundry project for smart contract development

Base Sepolia Deployment

Tests

Added united tests & base-sepolia forked integration tests

Checklist

  • I have formatted and linted my code
  • All new and existing tests pass
  • My commits are signed (required for merge) -- you may need to rebase if you initially pushed unsigned commits

@cb-heimdall
Copy link

cb-heimdall commented Jan 7, 2026

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@vercel
Copy link

vercel bot commented Jan 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
x402 Ready Ready Preview, Comment Jan 21, 2026 8:37pm

Request Review

@github-actions github-actions bot added typescript sdk Changes to core v2 packages labels Jan 7, 2026
@CarsonRoscoe CarsonRoscoe force-pushed the feat/exact-evm-permit-2 branch from bb465c0 to 54a250b Compare January 7, 2026 02:29
@CarsonRoscoe CarsonRoscoe changed the title feat: added and implemented @x402/evm-contracts feat: added @x402/evm-contracts Jan 7, 2026
@CarsonRoscoe CarsonRoscoe changed the title feat: added @x402/evm-contracts feat: added @x402/evm-contracts for evm permit2 update Jan 7, 2026
@CarsonRoscoe CarsonRoscoe force-pushed the feat/exact-evm-permit-2 branch from 54a250b to fe9a520 Compare January 7, 2026 02:40
@github-actions github-actions bot added the examples Changes to examples label Jan 7, 2026
@CarsonRoscoe CarsonRoscoe requested review from fabrice-cheng and removed request for fabrice-cheng January 7, 2026 18:13
Comment on lines 178 to 180
// Validate time window
if (block.timestamp < witness.validAfter) revert PaymentTooEarly();
if (block.timestamp > witness.validBefore) revert PaymentExpired();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Natspec above says witness.validAfter is the earliest timestamp that is acceptable. Thus, control should be expressed using <=. Same for validBefore.

Copy link
Contributor Author

@CarsonRoscoe CarsonRoscoe Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there was slight confusion in reviewing this, please verify my understanding.

if (block.timestamp < witness.validAfter) revert PaymentTooEarly() means that if the current timestamp is earlier than the earliest valid timestamp, revert

Similarly, if (block.timestamp > witness.validBefore) revert PaymentExpired() means that if the current timestamp is later than the last valid timestamp, revert

I believe that is correct, as <= and >= would then revert when equal, becoming exclusive. We want it to be inclusive

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man, yep you got it right. We're describing the revert edges. My bad!

Comment on lines 182 to 183
// Validate amount
if (amount > permit.permitted.amount) revert AmountExceedsPermitted();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be amount >= permit.permitted.amount

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the current code if (amount > permit.permitted.amount) is correct. This allows amount == permitted.amount (exact permitted amount) and only reverts when exceeding. Using >= would reject exact matches, breaking valid use cases where users want to transfer exactly what they permitted.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

@stevieraykatz
Copy link

Missing dep on OZ's reentrancy guard. Need to forge install OpenZeppelin/openzeppelin-contracts

Comment on lines 197 to 198
// Emit event for observability
emit X402PermitTransfer(owner, transferDetails.to, transferDetails.requestedAmount, permit.permitted.token);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all the data emitted here is redundant considering the events that will be emitted by the ERC20 contract. Is there a specific reason we need to emit this data ourselves?

Copy link
Contributor Author

@CarsonRoscoe CarsonRoscoe Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points. Reached out to the data scientist who will eventually be observing these contracts to get his opinion on if he wants/needs any events or not, given the redundancy. Will let his decision guide what we emit

@CarsonRoscoe CarsonRoscoe force-pushed the feat/exact-evm-permit-2 branch from 49a756b to ead5a1f Compare January 14, 2026 01:56
event Settled();

/// @notice Emitted when settleWithPermit() completes successfully
event SettledWithPermit();
Copy link
Contributor

@fabrice-cheng fabrice-cheng Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really see a value of being able to distinguish Settled vs SettledWithPermit()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe it's strictly necessary, however I do prefer having it to future proof our observability.

Without these events, there is no event driven approach for a data scientist to determine which transactions are flowing through the 2612 path, or through a separate erc20 approval. I foresee us wanting to know the breakdowns of transfer methods in the future, which is why I added this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% agreed on this take ^

* @dev Reverts if _permit2 is the zero address
*/
constructor(
address _permit2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you double check that adding this to the constructor with potential diff values won't affect the deployed contract address?

Unsure if the bytecode for CREATE2/CREATE3 is affected by constructor payload

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, a change in constructor arguments would indeed change the deployed address

Copy link
Contributor Author

@CarsonRoscoe CarsonRoscoe Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor parameter I am planning to use is Uniswap's canonical Permit2 contract address. Based on these docs, the address should exist on 16 different mainnets, which is a decent range but also limiting.

We could resolve with a separate initialize(_permit2) function gated by an initializer modifier to ensure it's only called once, and add a initialized() modifier on the settle calls to ensure initialize() was called before the contract gets used

Thoughts on that @fabrice-cheng ?


// Validate time window
if (block.timestamp < witness.validAfter) revert PaymentTooEarly();
if (block.timestamp > witness.validBefore) revert PaymentExpired();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi, Permit2 has a deadline in it, so this field would be redundant
`

struct PermitTransferFrom {
TokenPermissions permitted;
// a unique value for every token owner's signature to prevent signature replays
uint256 nonce;
// deadline on the permit signature
uint256 deadline;
}
`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, addressing now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci examples Changes to examples sdk Changes to core v2 packages specs Spec changes or additions typescript

Development

Successfully merging this pull request may close these issues.

5 participants