This repository was archived by the owner on Feb 5, 2026. It is now read-only.
generated from ScopeLift/foundry-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Implement Permitter hook contract for KYC auctions #1
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| [submodule "lib/forge-std"] | ||
| path = lib/forge-std | ||
| url = https://github.com/foundry-rs/forge-std | ||
| [submodule "lib/openzeppelin-contracts"] | ||
| path = lib/openzeppelin-contracts | ||
| url = https://github.com/OpenZeppelin/openzeppelin-contracts |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule openzeppelin-contracts
added at
fcbae5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.30; | ||
|
|
||
| import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
| import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; | ||
| import {IPermitter} from "./interfaces/IPermitter.sol"; | ||
|
|
||
| /// @title Permitter | ||
| /// @notice Implements Uniswap CCA ValidationHook interface for bid validation using EIP-712 signed | ||
| /// permits. Enforces KYC-based permissions and caps on token sales. | ||
| /// @dev Uses EIP-712 signatures for gasless permit verification. The domain separator includes | ||
| /// chainId and verifyingContract to prevent cross-chain and cross-auction replay attacks. | ||
| contract Permitter is IPermitter, EIP712 { | ||
| /// @notice EIP-712 typehash for the Permit struct. | ||
| bytes32 public constant PERMIT_TYPEHASH = | ||
| keccak256("Permit(address bidder,uint256 maxBidAmount,uint256 expiry)"); | ||
|
|
||
| /// @notice Address authorized to sign permits. | ||
| address public trustedSigner; | ||
|
|
||
| /// @notice Maximum total ETH that can be raised. | ||
| uint256 public maxTotalEth; | ||
|
|
||
| /// @notice Maximum tokens any single bidder can purchase. | ||
| uint256 public maxTokensPerBidder; | ||
|
|
||
| /// @notice Cumulative bid amounts per address. | ||
| mapping(address bidder => uint256 amount) public cumulativeBids; | ||
|
|
||
| /// @notice Total ETH raised across all bidders. | ||
| uint256 public totalEthRaised; | ||
|
|
||
| /// @notice Owner address that can update caps and pause. | ||
| address public owner; | ||
|
|
||
| /// @notice Whether the contract is paused. | ||
| bool public paused; | ||
|
|
||
| /// @notice Modifier to restrict access to owner only. | ||
| modifier onlyOwner() { | ||
| if (msg.sender != owner) revert Unauthorized(); | ||
| _; | ||
| } | ||
|
|
||
| /// @notice Creates a new Permitter instance. | ||
| /// @param _trustedSigner Address authorized to sign permits. | ||
| /// @param _maxTotalEth Maximum total ETH that can be raised. | ||
| /// @param _maxTokensPerBidder Maximum tokens any single bidder can purchase. | ||
| /// @param _owner Address that can update caps and pause. | ||
| constructor( | ||
| address _trustedSigner, | ||
| uint256 _maxTotalEth, | ||
| uint256 _maxTokensPerBidder, | ||
| address _owner | ||
| ) EIP712("Permitter", "1") { | ||
| if (_trustedSigner == address(0)) revert InvalidTrustedSigner(); | ||
| if (_owner == address(0)) revert InvalidOwner(); | ||
|
|
||
| trustedSigner = _trustedSigner; | ||
| maxTotalEth = _maxTotalEth; | ||
| maxTokensPerBidder = _maxTokensPerBidder; | ||
| owner = _owner; | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitter | ||
| function validateBid( | ||
| address bidder, | ||
| uint256 bidAmount, | ||
| uint256 ethValue, | ||
| bytes calldata permitData | ||
| ) external returns (bool valid) { | ||
| // 1. CHEAPEST: Check if paused | ||
| if (paused) revert ContractPaused(); | ||
|
|
||
| // 2. Decode permit data | ||
| (Permit memory permit, bytes memory signature) = abi.decode(permitData, (Permit, bytes)); | ||
|
|
||
| // 3. CHEAP: Check time window | ||
| if (block.timestamp > permit.expiry) { | ||
| revert SignatureExpired(permit.expiry, block.timestamp); | ||
| } | ||
|
|
||
| // 4. MODERATE: Verify EIP-712 signature | ||
| address recovered = _recoverSigner(permit, signature); | ||
| if (recovered != trustedSigner) revert InvalidSignature(trustedSigner, recovered); | ||
|
|
||
| // 5. Check permit is for this bidder | ||
| if (permit.bidder != bidder) revert InvalidSignature(bidder, permit.bidder); | ||
|
|
||
| // 6. STORAGE READ: Check individual cap | ||
| uint256 alreadyBid = cumulativeBids[bidder]; | ||
| uint256 newCumulative = alreadyBid + bidAmount; | ||
| if (newCumulative > permit.maxBidAmount) { | ||
| revert ExceedsPersonalCap(bidAmount, permit.maxBidAmount, alreadyBid); | ||
| } | ||
|
|
||
| // Also check against global maxTokensPerBidder if it's lower | ||
| if (newCumulative > maxTokensPerBidder) { | ||
| revert ExceedsPersonalCap(bidAmount, maxTokensPerBidder, alreadyBid); | ||
| } | ||
|
|
||
| // 7. STORAGE READ: Check global cap | ||
| uint256 alreadyRaised = totalEthRaised; | ||
| uint256 newTotalEth = alreadyRaised + ethValue; | ||
| if (newTotalEth > maxTotalEth) revert ExceedsTotalCap(ethValue, maxTotalEth, alreadyRaised); | ||
|
|
||
| // 8. STORAGE WRITE: Update state | ||
| cumulativeBids[bidder] = newCumulative; | ||
| totalEthRaised = newTotalEth; | ||
|
|
||
| // 9. Emit event for monitoring | ||
| emit PermitVerified( | ||
| bidder, bidAmount, permit.maxBidAmount - newCumulative, maxTotalEth - newTotalEth | ||
| ); | ||
|
|
||
| return true; | ||
| } | ||
|
Check notice on line 117 in src/Permitter.sol
|
||
|
|
||
| /// @inheritdoc IPermitter | ||
| function updateMaxTotalEth(uint256 newMaxTotalEth) external onlyOwner { | ||
| uint256 oldCap = maxTotalEth; | ||
| maxTotalEth = newMaxTotalEth; | ||
| emit CapUpdated(CapType.TOTAL_ETH, oldCap, newMaxTotalEth); | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitter | ||
| function updateMaxTokensPerBidder(uint256 newMaxTokensPerBidder) external onlyOwner { | ||
| uint256 oldCap = maxTokensPerBidder; | ||
| maxTokensPerBidder = newMaxTokensPerBidder; | ||
| emit CapUpdated(CapType.TOKENS_PER_BIDDER, oldCap, newMaxTokensPerBidder); | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitter | ||
| function updateTrustedSigner(address newSigner) external onlyOwner { | ||
| if (newSigner == address(0)) revert InvalidTrustedSigner(); | ||
| address oldSigner = trustedSigner; | ||
| trustedSigner = newSigner; | ||
| emit SignerUpdated(oldSigner, newSigner); | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitter | ||
| function pause() external onlyOwner { | ||
| paused = true; | ||
| emit Paused(msg.sender); | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitter | ||
| function unpause() external onlyOwner { | ||
| paused = false; | ||
| emit Unpaused(msg.sender); | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitter | ||
| function getBidAmount(address bidder) external view returns (uint256 cumulativeBid) { | ||
| return cumulativeBids[bidder]; | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitter | ||
| function getTotalEthRaised() external view returns (uint256) { | ||
| return totalEthRaised; | ||
| } | ||
|
|
||
| /// @notice Get the EIP-712 domain separator. | ||
| /// @return The domain separator hash. | ||
| function domainSeparator() external view returns (bytes32) { | ||
| return _domainSeparatorV4(); | ||
| } | ||
|
|
||
| /// @notice Recover the signer address from a permit and signature. | ||
| /// @param permit The permit struct. | ||
| /// @param signature The EIP-712 signature. | ||
| /// @return The recovered signer address. | ||
| function _recoverSigner(Permit memory permit, bytes memory signature) | ||
| internal | ||
| view | ||
| returns (address) | ||
| { | ||
| bytes32 structHash = | ||
| keccak256(abi.encode(PERMIT_TYPEHASH, permit.bidder, permit.maxBidAmount, permit.expiry)); | ||
| bytes32 digest = _hashTypedDataV4(structHash); | ||
| return ECDSA.recover(digest, signature); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.30; | ||
|
|
||
| import {IPermitterFactory} from "./interfaces/IPermitterFactory.sol"; | ||
| import {Permitter} from "./Permitter.sol"; | ||
|
|
||
| /// @title PermitterFactory | ||
| /// @notice Factory contract for deploying isolated Permitter instances for each auction using | ||
| /// CREATE2 for deterministic addresses. | ||
| /// @dev Deploy this factory with CREATE2 using the same salt on all chains to get the same factory | ||
| /// address across networks. | ||
| contract PermitterFactory is IPermitterFactory { | ||
| /// @inheritdoc IPermitterFactory | ||
| function createPermitter( | ||
| address trustedSigner, | ||
| uint256 maxTotalEth, | ||
| uint256 maxTokensPerBidder, | ||
| address owner, | ||
| bytes32 salt | ||
| ) external returns (address permitter) { | ||
| // Compute the final salt using the sender address to prevent front-running | ||
| bytes32 finalSalt = keccak256(abi.encodePacked(msg.sender, salt)); | ||
|
|
||
| // Deploy the Permitter using CREATE2 | ||
| permitter = address( | ||
| new Permitter{salt: finalSalt}(trustedSigner, maxTotalEth, maxTokensPerBidder, owner) | ||
| ); | ||
|
|
||
| emit PermitterCreated(permitter, owner, trustedSigner, maxTotalEth, maxTokensPerBidder); | ||
| } | ||
|
|
||
| /// @inheritdoc IPermitterFactory | ||
| function predictPermitterAddress( | ||
| address trustedSigner, | ||
| uint256 maxTotalEth, | ||
| uint256 maxTokensPerBidder, | ||
| address owner, | ||
| bytes32 salt | ||
| ) external view returns (address) { | ||
| // Compute the final salt the same way as in createPermitter | ||
| bytes32 finalSalt = keccak256(abi.encodePacked(msg.sender, salt)); | ||
|
|
||
| // Compute the init code hash | ||
| bytes memory initCode = abi.encodePacked( | ||
| type(Permitter).creationCode, | ||
| abi.encode(trustedSigner, maxTotalEth, maxTokensPerBidder, owner) | ||
| ); | ||
| bytes32 initCodeHash = keccak256(initCode); | ||
|
|
||
| // Compute the CREATE2 address | ||
| return address( | ||
| uint160( | ||
| uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), finalSalt, initCodeHash))) | ||
| ) | ||
| ); | ||
| } | ||
|
Check warning on line 56 in src/PermitterFactory.sol
|
||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / Slither
State variables that could be declared immutable Warning