Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.

Conversation

@rsolari
Copy link
Collaborator

@rsolari rsolari commented Dec 31, 2025

Summary

Implements the Permitter hook contract system for enforcing KYC-based permissions and bid caps on Uniswap CCA token auctions. Features EIP-712 signed permits, CREATE2 factory deployment, and comprehensive validation logic.

Changes

  • Permitter.sol: Core validation contract with signature verification, cap enforcement, and pausable controls
  • PermitterFactory.sol: CREATE2 factory for deterministic contract deployment across chains
  • IPermitter & IPermitterFactory: Complete interfaces with error definitions and events
  • 69 unit, integration, and fuzz tests: Full coverage of all functionality with all tests passing

Test Plan

  • Run forge test to verify all 69 tests pass
  • Contracts compile without errors at Solc 0.8.30
  • No security vulnerabilities from OpenZeppelin dependencies

🤖 Generated with Claude Code

…tions

Adds EIP-712 signed permits for enforcing KYC approvals and bid caps on Uniswap CCA auctions. Includes PermitterFactory for CREATE2 deployments, comprehensive validation logic, and 69 tests covering units, integration, and fuzz scenarios. All tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
uint256 public totalEthRaised;

/// @notice Owner address that can update caps and pause.
address public owner;

Check warning

Code scanning / Slither

State variables that could be declared immutable Warning

Permitter.owner should be immutable
Comment on lines 33 to 54
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))))
);
}
Comment on lines 63 to 120
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

Code scanning / Slither

Block timestamp Low

Permitter.validateBid(address,uint256,uint256,bytes) uses timestamp for comparisons
Dangerous comparisons:
- block.timestamp > permit.expiry
rsolari and others added 2 commits December 31, 2025 15:00
- Rename test_Reverts* to test_RevertIf_* for scopelint compliance
- Fix fuzz tests that used vm.assume on array lengths causing too many rejections
- Replace array fuzz params with bound() on numBids/seed for deterministic random generation
- Apply scopelint formatting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The lcov-reporter-action requires write permission to post coverage
comments on PRs. Added the permissions block to the coverage job.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions
Copy link

Coverage after merging rsolari/permit-hook-contract into main will be

100.00%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   Permitter.sol100%100%100%100%
   PermitterFactory.sol100%100%100%100%

@rsolari rsolari merged commit 75c4229 into main Dec 31, 2025
6 checks passed
@rsolari rsolari deleted the rsolari/permit-hook-contract branch December 31, 2025 21:46
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant