Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 46 additions & 47 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,52 @@ DelegatecallGuardTest:test_direct_call_reverts_NotDelegateCall() (gas: 8420)
DelegatecallGuardTest:test_multiple_delegatecall_guards() (gas: 298825)
DelegatecallGuardTest:test_onlyDelegatecall_modifier_usage() (gas: 98867)
DelegatecallGuardTest:test_self_address_immutable() (gas: 2877)
TrailsIntentEntrypointDeploymentTest:test_DeployIntentEntrypoint_SameAddress() (gas: 840130)
TrailsIntentEntrypointDeploymentTest:test_DeployIntentEntrypoint_Success() (gas: 833384)
TrailsIntentEntrypointDeploymentTest:test_DeployedIntentEntrypoint_HasCorrectConfiguration() (gas: 836260)
TrailsIntentEntrypointTest:testAssemblyCodeExecution() (gas: 90673)
TrailsIntentEntrypointTest:testConstructor() (gas: 3419)
TrailsIntentEntrypointTest:testConstructorAndDomainSeparator() (gas: 8515)
TrailsIntentEntrypointTest:testDepositToIntentAlreadyUsed() (gas: 115475)
TrailsIntentEntrypointTest:testDepositToIntentCannotReuseDigest() (gas: 96203)
TrailsIntentEntrypointTest:testDepositToIntentExpiredDeadline() (gas: 56783)
TrailsIntentEntrypointTest:testDepositToIntentReentrancyProtection() (gas: 115453)
TrailsIntentEntrypointTest:testDepositToIntentRequiresNonZeroAmount() (gas: 28081)
TrailsIntentEntrypointTest:testDepositToIntentRequiresValidToken() (gas: 31486)
TrailsIntentEntrypointTest:testDepositToIntentTransferFromFails() (gas: 59771)
TrailsIntentEntrypointTest:testDepositToIntentWithFeeAmountButNoCollector_Reverts() (gas: 113429)
TrailsIntentEntrypointTest:testDepositToIntentWithFeeCollectorButNoAmount_Reverts() (gas: 91375)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitAlreadyUsed() (gas: 134746)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitExpiredDeadline() (gas: 37222)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitReentrancyProtection() (gas: 125501)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitRequiresNonZeroAmount() (gas: 35914)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitRequiresPermitAmount() (gas: 112450)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitRequiresValidToken() (gas: 36640)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitTransferFromFails() (gas: 111613)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitWithFeeAmountButNoCollector_Reverts() (gas: 141960)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitWithFeeCollectorButNoAmount_Reverts() (gas: 121561)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitWrongSigner() (gas: 41835)
TrailsIntentEntrypointTest:testDepositToIntentWithoutPermit_RequiresIntentAddress() (gas: 32195)
TrailsIntentEntrypointTest:testDepositToIntentWrongSigner() (gas: 58611)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20AndFee_Success() (gas: 810900)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20_InsufficientAllowance_Reverts() (gas: 773649)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20_InsufficientBalance_Reverts() (gas: 773630)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20_Success() (gas: 781820)
TrailsIntentEntrypointTest:testExactApprovalFlow() (gas: 152574)
TrailsIntentEntrypointTest:testExecuteIntentWithFee() (gas: 155007)
TrailsIntentEntrypointTest:testExecuteIntentWithPermit() (gas: 124649)
TrailsIntentEntrypointTest:testExecuteIntentWithPermitExpired() (gas: 37141)
TrailsIntentEntrypointTest:testExecuteIntentWithPermitInvalidSignature() (gas: 39013)
TrailsIntentEntrypointTest:testExecuteIntentWithPermit_permit_frontrun() (gas: 129489)
TrailsIntentEntrypointTest:testFeeCollectorReceivesFees() (gas: 149159)
TrailsIntentEntrypointTest:testFeeCollectorReceivesFeesWithoutPermit() (gas: 118818)
TrailsIntentEntrypointTest:testInfiniteApprovalFlow() (gas: 146864)
TrailsIntentEntrypointTest:testIntentTypehashConstant() (gas: 5754)
TrailsIntentEntrypointTest:testInvalidNonceReverts() (gas: 55344)
TrailsIntentEntrypointTest:testNonceIncrementsOnDeposit() (gas: 91852)
TrailsIntentEntrypointTest:testPermitAmountExcessiveThenUseRemainingAllowance() (gas: 178584)
TrailsIntentEntrypointTest:testPermitAmountExcessiveWithFeeLeavesAllowance() (gas: 174910)
TrailsIntentEntrypointTest:testPermitAmountInsufficientWithFee() (gas: 142114)
TrailsIntentEntrypointTest:testVersionConstant() (gas: 10550)
TrailsIntentEntrypointDeploymentTest:test_DeployIntentEntrypoint_SameAddress() (gas: 836930)
TrailsIntentEntrypointDeploymentTest:test_DeployIntentEntrypoint_Success() (gas: 830162)
TrailsIntentEntrypointDeploymentTest:test_DeployedIntentEntrypoint_HasCorrectConfiguration() (gas: 833038)
TrailsIntentEntrypointTest:testAssemblyCodeExecution() (gas: 91047)
TrailsIntentEntrypointTest:testConstructor() (gas: 3374)
TrailsIntentEntrypointTest:testConstructorAndDomainSeparator() (gas: 8632)
TrailsIntentEntrypointTest:testDepositToIntentAlreadyUsed() (gas: 116089)
TrailsIntentEntrypointTest:testDepositToIntentCannotReuseDigest() (gas: 96752)
TrailsIntentEntrypointTest:testDepositToIntentExpiredDeadline(uint256,uint256) (runs: 257, μ: 61561, ~: 61681)
TrailsIntentEntrypointTest:testDepositToIntentReentrancyProtection() (gas: 116067)
TrailsIntentEntrypointTest:testDepositToIntentRequiresNonZeroAmount() (gas: 28499)
TrailsIntentEntrypointTest:testDepositToIntentRequiresValidToken() (gas: 31940)
TrailsIntentEntrypointTest:testDepositToIntentTransferFromFails() (gas: 60141)
TrailsIntentEntrypointTest:testDepositToIntentWithFeeAmountButNoCollector_Reverts() (gas: 113938)
TrailsIntentEntrypointTest:testDepositToIntentWithFeeCollectorButNoAmount_Reverts() (gas: 91874)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitAlreadyUsed() (gas: 113409)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitExpiredDeadline() (gas: 117103)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitReentrancyProtection() (gas: 122088)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitRequiresNonZeroAmount() (gas: 36165)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitRequiresPermitAmount() (gas: 109379)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitRequiresValidToken() (gas: 26812)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitTransferFromFails() (gas: 108389)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitWithFeeAmountButNoCollector_Reverts() (gas: 138776)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitWithFeeCollectorButNoAmount_Reverts() (gas: 118389)
TrailsIntentEntrypointTest:testDepositToIntentWithPermitWrongSigner() (gas: 79036)
TrailsIntentEntrypointTest:testDepositToIntentWithoutPermit_RequiresIntentAddress() (gas: 32735)
TrailsIntentEntrypointTest:testDepositToIntentWrongSigner() (gas: 79273)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20AndFee_Success() (gas: 811747)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20_InsufficientAllowance_Reverts() (gas: 773987)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20_InsufficientBalance_Reverts() (gas: 774081)
TrailsIntentEntrypointTest:testDepositToIntent_WithNonStandardERC20_Success() (gas: 782393)
TrailsIntentEntrypointTest:testExactApprovalFlow() (gas: 147221)
TrailsIntentEntrypointTest:testExecuteIntentWithFee() (gas: 152148)
TrailsIntentEntrypointTest:testExecuteIntentWithPermit() (gas: 121454)
TrailsIntentEntrypointTest:testExecuteIntentWithPermitExpired() (gas: 117004)
TrailsIntentEntrypointTest:testExecuteIntentWithPermitInvalidSignature() (gas: 80422)
TrailsIntentEntrypointTest:testFeeCollectorReceivesFees() (gas: 145266)
TrailsIntentEntrypointTest:testFeeCollectorReceivesFeesWithoutPermit() (gas: 119441)
TrailsIntentEntrypointTest:testInfiniteApprovalFlow() (gas: 144569)
TrailsIntentEntrypointTest:testIntentTypehashConstant() (gas: 5752)
TrailsIntentEntrypointTest:testInvalidNonceReverts() (gas: 55864)
TrailsIntentEntrypointTest:testNonceIncrementsOnDeposit() (gas: 92337)
TrailsIntentEntrypointTest:testPermitAmountExcessiveThenUseRemainingAllowance() (gas: 175632)
TrailsIntentEntrypointTest:testPermitAmountExcessiveWithFeeLeavesAllowance() (gas: 171462)
TrailsIntentEntrypointTest:testPermitAmountInsufficientWithFee() (gas: 138462)
TrailsIntentEntrypointTest:testVersionConstant() (gas: 10493)
TrailsRouterDeploymentTest:test_DeployTrailsRouter_SameAddress() (gas: 1864438)
TrailsRouterDeploymentTest:test_DeployTrailsRouter_Success() (gas: 1852254)
TrailsRouterDeploymentTest:test_DeployedRouter_HasCorrectConfiguration() (gas: 1852050)
Expand Down
122 changes: 70 additions & 52 deletions src/TrailsIntentEntrypoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
bytes32 private constant EIP712_DOMAIN_NAME = keccak256(bytes("TrailsIntentEntrypoint"));
bytes32 private constant EIP712_DOMAIN_VERSION = keccak256(bytes(VERSION));

// Mask to ensure deadline hash is always in the future
uint256 private constant DEADLINE_MASK = 0xff00000000000000000000000000000000000000000000000000000000000000;

// -------------------------------------------------------------------------
// Errors
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -73,24 +76,17 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
uint256 nonce,
uint256 feeAmount,
address feeCollector,
uint8 permitV,
bytes32 permitR,
bytes32 permitS,
uint8 sigV,
bytes32 sigR,
bytes32 sigS
) external nonReentrant {
_verifyAndMarkIntent(
user, token, amount, intentAddress, deadline, nonce, feeAmount, feeCollector, sigV, sigR, sigS
);
// Validate intent parameters and increment nonce (digest validation is nested within permit execution)
bytes32 intentDigest =
_prepareIntentUsage(user, token, amount, intentAddress, deadline, nonce, feeAmount, feeCollector);
uint256 permitDeadline = uint256(intentDigest) | DEADLINE_MASK;

// Execute permit with try-catch to handle potential frontrunning, and scope variables to avoid stack too deep
try IERC20Permit(token).permit(user, address(this), permitAmount, deadline, permitV, permitR, permitS) {
// Permit succeeded
}
catch {
// Permit may have been frontrun. Continue with transferFrom attempt.
}
// Execute permit
IERC20Permit(token).permit(user, address(this), permitAmount, permitDeadline, sigV, sigR, sigS);

_processDeposit(user, token, amount, intentAddress, feeAmount, feeCollector);
}
Expand All @@ -109,63 +105,48 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
bytes32 sigR,
bytes32 sigS
) external nonReentrant {
_verifyAndMarkIntent(
user, token, amount, intentAddress, deadline, nonce, feeAmount, feeCollector, sigV, sigR, sigS
bytes32 intentDigest = _prepareIntentUsage(
user, token, amount, intentAddress, deadline, nonce, feeAmount, feeCollector
);
_processDeposit(user, token, amount, intentAddress, feeAmount, feeCollector);
}

function _processDeposit(
address user,
address token,
uint256 amount,
address intentAddress,
uint256 feeAmount,
address feeCollector
) internal {
IERC20(token).safeTransferFrom(user, intentAddress, amount);
_verifyIntentSignature(intentDigest, sigV, sigR, sigS, user);

// Pay fee if specified (fee token is same as deposit token)
bool feeAmountSupplied = feeAmount > 0;
bool feeCollectorSupplied = feeCollector != address(0);
if (feeAmountSupplied != feeCollectorSupplied) {
// Must supply both feeAmount and feeCollector, or neither
revert InvalidFeeParameters();
}
if (feeAmountSupplied && feeCollectorSupplied) {
IERC20(token).safeTransferFrom(user, feeCollector, feeAmount);
emit FeePaid(user, token, feeAmount, feeCollector);
}

emit IntentDeposit(user, intentAddress, amount);
_processDeposit(user, token, amount, intentAddress, feeAmount, feeCollector);
}

// -------------------------------------------------------------------------
// Internal Functions
// -------------------------------------------------------------------------

/// forge-lint: disable-next-line(mixed-case-function)
function _verifyAndMarkIntent(
/// @notice Prepares intent usage by validating parameters, building intent digest, and incrementing nonce
/// @dev If deadline is 0, skips expiration check (used for permit flow where deadline is computed)
/// @param user The user making the deposit
/// @param token The token to deposit
/// @param amount The amount to deposit
/// @param intentAddress The intent address to deposit to
/// @param deadline The intent deadline (0 to skip expiration check)
/// @param nonce The nonce for this user
/// @param feeAmount The amount of fee to pay
/// @param feeCollector The address to receive the fee
/// @return intentDigest The EIP-712 digest of the intent message
function _prepareIntentUsage(
address user,
address token,
uint256 amount,
address intentAddress,
uint256 deadline,
uint256 nonce,
uint256 feeAmount,
address feeCollector,
uint8 sigV,
bytes32 sigR,
bytes32 sigS
) internal {
address feeCollector
) internal returns (bytes32 intentDigest) {
// Validate parameters
if (amount == 0) revert InvalidAmount();
if (token == address(0)) revert InvalidToken();
if (intentAddress == address(0)) revert InvalidIntentAddress();
if (block.timestamp > deadline) revert IntentExpired();
// Chain ID is already included in the signature, so we don't need to check it here
// The signature verification will fail if the chain ID doesn't match
if (nonce != nonces[user]) revert InvalidNonce();

// Build intent hash
bytes32 _typehash = TRAILS_INTENT_TYPEHASH;
bytes32 intentHash;
// keccak256(abi.encode(TRAILS_INTENT_TYPEHASH, user, token, amount, intentAddress, deadline, chainId, nonce, feeAmount, feeCollector));
Expand All @@ -184,20 +165,57 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
intentHash := keccak256(ptr, 0x140)
}

// Build intent digest
bytes32 _domainSeparator = DOMAIN_SEPARATOR();
bytes32 digest;
// keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, intentHash));
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x1901)
mstore(add(ptr, 0x20), _domainSeparator)
mstore(add(ptr, 0x40), intentHash)
digest := keccak256(add(ptr, 0x1e), 0x42)
intentDigest := keccak256(add(ptr, 0x1e), 0x42)
}
address recovered = ECDSA.recover(digest, sigV, sigR, sigS);
if (recovered != user) revert InvalidIntentSignature();

// Increment nonce for the user
nonces[user]++;
}

/// @notice Verifies that the intent signature is valid
/// @param intentDigest The EIP-712 digest of the intent message
/// @param sigV The signature v component
/// @param sigR The signature r component
/// @param sigS The signature s component
/// @param expectedUser The expected user address that signed the intent
function _verifyIntentSignature(bytes32 intentDigest, uint8 sigV, bytes32 sigR, bytes32 sigS, address expectedUser)
internal
pure
{
address recovered = ECDSA.recover(intentDigest, sigV, sigR, sigS);
if (recovered != expectedUser) revert InvalidIntentSignature();
}

function _processDeposit(
address user,
address token,
uint256 amount,
address intentAddress,
uint256 feeAmount,
address feeCollector
) internal {
IERC20(token).safeTransferFrom(user, intentAddress, amount);

// Pay fee if specified (fee token is same as deposit token)
bool feeAmountSupplied = feeAmount > 0;
bool feeCollectorSupplied = feeCollector != address(0);
if (feeAmountSupplied != feeCollectorSupplied) {
// Must supply both feeAmount and feeCollector, or neither
revert InvalidFeeParameters();
}
if (feeAmountSupplied && feeCollectorSupplied) {
IERC20(token).safeTransferFrom(user, feeCollector, feeAmount);
emit FeePaid(user, token, feeAmount, feeCollector);
}

emit IntentDeposit(user, intentAddress, amount);
}
}
18 changes: 7 additions & 11 deletions src/interfaces/ITrailsIntentEntrypoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,20 @@ interface ITrailsIntentEntrypoint {
// -------------------------------------------------------------------------

/// @notice Deposit tokens to an intent address using ERC20 permit
/// @dev The intent digest is encoded into the permit deadline with the highest byte set to 0xff.
/// @dev The permit signature must be valid for the permit with the computed deadline.
/// @param user The user making the deposit
/// @param token The token to deposit (also used for fee payment)
/// @param amount The amount to deposit
/// @param permitAmount The allowance to set via permit (must cover amount + feeAmount; can be higher to leave leftover)
/// @param permitAmount The amount to permit for spending (amount + feeAmount if paying fee)
/// @param intentAddress The intent address to deposit to
/// @param deadline The permit deadline
/// @param deadline The intent deadline
/// @param nonce The nonce for this user
/// @param feeAmount The amount of fee to pay (0 for no fee, paid in same token)
/// @param feeCollector The address to receive the fee (address(0) for no fee)
/// @param permitV The permit signature v component
/// @param permitR The permit signature r component
/// @param permitS The permit signature s component
/// @param sigV The intent signature v component
/// @param sigR The intent signature r component
/// @param sigS The intent signature s component
/// @param sigV The signature v component
/// @param sigR The signature r component
/// @param sigS The signature s component
function depositToIntentWithPermit(
address user,
address token,
Expand All @@ -71,9 +70,6 @@ interface ITrailsIntentEntrypoint {
uint256 nonce,
uint256 feeAmount,
address feeCollector,
uint8 permitV,
bytes32 permitR,
bytes32 permitS,
uint8 sigV,
bytes32 sigR,
bytes32 sigS
Expand Down
Loading