Skip to content

Commit fab84de

Browse files
committed
Resolve merge conflicts and fix test argument count after combining permit handling logic
2 parents 36ccb16 + 0c40a8f commit fab84de

File tree

7 files changed

+797
-468
lines changed

7 files changed

+797
-468
lines changed

.gas-snapshot

Lines changed: 113 additions & 111 deletions
Large diffs are not rendered by default.

src/TrailsIntentEntrypoint.sol

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
2323
// -------------------------------------------------------------------------
2424

2525
bytes32 public constant TRAILS_INTENT_TYPEHASH = keccak256(
26-
"TrailsIntent(address user,address token,uint256 amount,address intentAddress,uint256 deadline,uint256 chainId,uint256 nonce,uint256 feeAmount,address feeCollector)"
26+
"TrailsIntent(string description,address user,address token,uint256 amount,address intentAddress,uint256 deadline,uint256 chainId,uint256 nonce,uint256 feeAmount,address feeCollector)"
2727
);
2828
string public constant VERSION = "1";
2929

@@ -84,27 +84,38 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
8484
uint256 nonce,
8585
uint256 feeAmount,
8686
address feeCollector,
87-
uint8 permitV,
88-
bytes32 permitR,
89-
bytes32 permitS,
90-
uint8 sigV,
91-
bytes32 sigR,
92-
bytes32 sigS
87+
string calldata description,
88+
ITrailsIntentEntrypoint.Signature calldata permitSig,
89+
ITrailsIntentEntrypoint.Signature calldata intentSig
9390
) external nonReentrant {
94-
_verifyAndMarkIntent(
95-
user, token, amount, intentAddress, deadline, nonce, feeAmount, feeCollector, sigV, sigR, sigS
96-
);
97-
9891
// Validate permitAmount exactly matches the total required amount (deposit + fee)
9992
// This prevents permit/approval mismatches that could cause DoS or unexpected behavior
10093
unchecked {
10194
if (permitAmount != amount + feeAmount) revert PermitAmountMismatch();
10295
}
10396

104-
try IERC20Permit(token).permit(user, address(this), permitAmount, deadline, permitV, permitR, permitS) {}
105-
catch {
97+
// Execute permit with try-catch to handle potential frontrunning, and scope variables to avoid stack too deep
98+
try IERC20Permit(token).permit(user, address(this), permitAmount, deadline, permitSig.v, permitSig.r, permitSig.s) {
99+
// Permit succeeded
100+
} catch {
106101
// Permit may have been frontrun. Continue with transferFrom attempt.
107102
}
103+
104+
_verifyAndMarkIntent(
105+
user,
106+
token,
107+
amount,
108+
intentAddress,
109+
deadline,
110+
nonce,
111+
feeAmount,
112+
feeCollector,
113+
description,
114+
intentSig.v,
115+
intentSig.r,
116+
intentSig.s
117+
);
118+
108119
IERC20(token).safeTransferFrom(user, intentAddress, amount);
109120

110121
// Pay fee if specified (fee token is same as deposit token)
@@ -126,12 +137,22 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
126137
uint256 nonce,
127138
uint256 feeAmount,
128139
address feeCollector,
129-
uint8 sigV,
130-
bytes32 sigR,
131-
bytes32 sigS
140+
string calldata description,
141+
ITrailsIntentEntrypoint.Signature calldata intentSig
132142
) external nonReentrant {
133143
_verifyAndMarkIntent(
134-
user, token, amount, intentAddress, deadline, nonce, feeAmount, feeCollector, sigV, sigR, sigS
144+
user,
145+
token,
146+
amount,
147+
intentAddress,
148+
deadline,
149+
nonce,
150+
feeAmount,
151+
feeCollector,
152+
description,
153+
intentSig.v,
154+
intentSig.r,
155+
intentSig.s
135156
);
136157

137158
IERC20(token).safeTransferFrom(user, intentAddress, amount);
@@ -159,6 +180,7 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
159180
uint256 nonce,
160181
uint256 feeAmount,
161182
address feeCollector,
183+
string calldata description,
162184
uint8 sigV,
163185
bytes32 sigR,
164186
bytes32 sigS
@@ -172,21 +194,23 @@ contract TrailsIntentEntrypoint is ReentrancyGuard, ITrailsIntentEntrypoint {
172194
if (nonce != nonces[user]) revert InvalidNonce();
173195

174196
bytes32 _typehash = TRAILS_INTENT_TYPEHASH;
197+
bytes32 descriptionHash = keccak256(bytes(description));
175198
bytes32 intentHash;
176-
// keccak256(abi.encode(TRAILS_INTENT_TYPEHASH, user, token, amount, intentAddress, deadline, chainId, nonce, feeAmount, feeCollector));
199+
// keccak256(abi.encode(TRAILS_INTENT_TYPEHASH, keccak256(bytes(description)), user, token, amount, intentAddress, deadline, chainId, nonce, feeAmount, feeCollector));
177200
assembly {
178201
let ptr := mload(0x40)
179202
mstore(ptr, _typehash)
180-
mstore(add(ptr, 0x20), user)
181-
mstore(add(ptr, 0x40), token)
182-
mstore(add(ptr, 0x60), amount)
183-
mstore(add(ptr, 0x80), intentAddress)
184-
mstore(add(ptr, 0xa0), deadline)
185-
mstore(add(ptr, 0xc0), chainid())
186-
mstore(add(ptr, 0xe0), nonce)
187-
mstore(add(ptr, 0x100), feeAmount)
188-
mstore(add(ptr, 0x120), feeCollector)
189-
intentHash := keccak256(ptr, 0x140)
203+
mstore(add(ptr, 0x20), descriptionHash)
204+
mstore(add(ptr, 0x40), user)
205+
mstore(add(ptr, 0x60), token)
206+
mstore(add(ptr, 0x80), amount)
207+
mstore(add(ptr, 0xa0), intentAddress)
208+
mstore(add(ptr, 0xc0), deadline)
209+
mstore(add(ptr, 0xe0), chainid())
210+
mstore(add(ptr, 0x100), nonce)
211+
mstore(add(ptr, 0x120), feeAmount)
212+
mstore(add(ptr, 0x140), feeCollector)
213+
intentHash := keccak256(ptr, 0x160)
190214
}
191215

192216
bytes32 _domainSeparator = DOMAIN_SEPARATOR;

src/TrailsRouter.sol

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,10 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
3535
error InvalidFunctionSelector(bytes4 selector);
3636
error AllowFailureMustBeFalse(uint256 callIndex);
3737
error SuccessSentinelNotSet();
38-
error NoEthSent();
38+
error NoValueAvailable();
3939
error NoTokensToPull();
40-
error InsufficientEth(uint256 required, uint256 received);
40+
error IncorrectValue(uint256 required, uint256 received);
4141
error NoTokensToSweep();
42-
error NoEthAvailable();
4342
error AmountOffsetOutOfBounds();
4443
error PlaceholderMismatch();
4544
error TargetCallFailed(bytes revertData);
@@ -71,7 +70,7 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
7170
{
7271
uint256 amount;
7372
if (token == address(0)) {
74-
if (msg.value == 0) revert NoEthSent();
73+
if (msg.value == 0) revert NoValueAvailable();
7574
amount = msg.value;
7675
} else {
7776
amount = _getBalance(token, msg.sender);
@@ -89,14 +88,27 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
8988
{
9089
_validateRouterCall(data);
9190
if (token == address(0)) {
92-
if (msg.value < amount) revert InsufficientEth(amount, msg.value);
91+
if (msg.value != amount) revert IncorrectValue(amount, msg.value);
9392
} else {
93+
if (msg.value != 0) revert IncorrectValue(0, msg.value);
9494
_safeTransferFrom(token, msg.sender, address(this), amount);
9595
}
9696

9797
(bool success, bytes memory returnData) = MULTICALL3.delegatecall(data);
9898
if (!success) revert TargetCallFailed(returnData);
99-
return abi.decode(returnData, (IMulticall3.Result[]));
99+
returnResults = abi.decode(returnData, (IMulticall3.Result[]));
100+
101+
// Sweep remaining balance back to msg.sender to prevent dust from EXACT_OUTPUT swaps getting stuck.
102+
// We sweep the full balance (not tracking initial) since TrailsRouter is stateless by design.
103+
uint256 remaining = _getSelfBalance(token);
104+
if (remaining > 0) {
105+
if (token == address(0)) {
106+
_transferNative(msg.sender, remaining);
107+
} else {
108+
_transferERC20(token, msg.sender, remaining);
109+
}
110+
emit Sweep(token, msg.sender, remaining);
111+
}
100112
}
101113

102114
// -------------------------------------------------------------------------
@@ -115,8 +127,9 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
115127

116128
if (token == address(0)) {
117129
callerBalance = msg.value;
118-
if (callerBalance == 0) revert NoEthSent();
130+
if (callerBalance == 0) revert NoValueAvailable();
119131
} else {
132+
if (msg.value != 0) revert IncorrectValue(0, msg.value);
120133
callerBalance = _getBalance(token, msg.sender);
121134
if (callerBalance == 0) revert NoTokensToSweep();
122135
_safeTransferFrom(token, msg.sender, address(this), callerBalance);
@@ -133,10 +146,14 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
133146
uint256 amountOffset,
134147
bytes32 placeholder
135148
) public payable {
149+
if (token == address(0) && msg.value != 0) {
150+
revert IncorrectValue(0, msg.value);
151+
}
152+
136153
uint256 callerBalance = _getSelfBalance(token);
137154
if (callerBalance == 0) {
138155
if (token == address(0)) {
139-
revert NoEthAvailable();
156+
revert NoValueAvailable();
140157
} else {
141158
revert NoTokensToSweep();
142159
}
@@ -304,7 +321,7 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
304321
uint256 callerBalance = _getSelfBalance(token);
305322
if (callerBalance == 0) {
306323
if (token == address(0)) {
307-
revert NoEthAvailable();
324+
revert NoValueAvailable();
308325
} else {
309326
revert NoTokensToSweep();
310327
}
@@ -361,19 +378,29 @@ contract TrailsRouter is IDelegatedExtension, ITrailsRouter, DelegatecallGuard,
361378

362379
bytes4 selector = bytes4(callData[0:4]);
363380

364-
// Only allow `aggregate3Value` calls (0x174dea71)
365-
if (selector != 0x174dea71) {
366-
revert InvalidFunctionSelector(selector);
367-
}
368-
369-
// Decode and validate the Call3Value[] array to ensure allowFailure=false for all calls
370-
IMulticall3.Call3Value[] memory calls = abi.decode(callData[4:], (IMulticall3.Call3Value[]));
381+
// Only allow `aggregate3Value` or `aggregate3` (0x174dea71 or 0x82ad56cb)
382+
if (selector == 0x174dea71) {
383+
// Decode and validate the Call3Value[] array to ensure allowFailure=false for all calls
384+
IMulticall3.Call3Value[] memory calls = abi.decode(callData[4:], (IMulticall3.Call3Value[]));
371385

372-
// Iterate through all calls and verify allowFailure is false
373-
for (uint256 i = 0; i < calls.length; i++) {
374-
if (calls[i].allowFailure) {
375-
revert AllowFailureMustBeFalse(i);
386+
// Iterate through all calls and verify allowFailure is false
387+
for (uint256 i = 0; i < calls.length; i++) {
388+
if (calls[i].allowFailure) {
389+
revert AllowFailureMustBeFalse(i);
390+
}
376391
}
392+
} else if (selector == 0x82ad56cb) {
393+
// Decode and validate the Call3[] array to ensure allowFailure=false for all calls
394+
IMulticall3.Call3[] memory calls = abi.decode(callData[4:], (IMulticall3.Call3[]));
395+
396+
// Iterate through all calls and verify allowFailure is false
397+
for (uint256 i = 0; i < calls.length; i++) {
398+
if (calls[i].allowFailure) {
399+
revert AllowFailureMustBeFalse(i);
400+
}
401+
}
402+
} else {
403+
revert InvalidFunctionSelector(selector);
377404
}
378405
}
379406
}

src/interfaces/ITrailsIntentEntrypoint.sol

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity ^0.8.20;
2+
pragma solidity ^0.8.30;
33

44
/// @title ITrailsIntentEntrypoint
55
/// @notice Interface for the TrailsIntentEntrypoint contract
66
interface ITrailsIntentEntrypoint {
7+
// -------------------------------------------------------------------------
8+
// Types
9+
// -------------------------------------------------------------------------
10+
11+
struct Signature {
12+
uint8 v;
13+
bytes32 r;
14+
bytes32 s;
15+
}
16+
717
// -------------------------------------------------------------------------
818
// Events
919
// -------------------------------------------------------------------------
@@ -55,12 +65,9 @@ interface ITrailsIntentEntrypoint {
5565
/// @param nonce The nonce for this user
5666
/// @param feeAmount The amount of fee to pay (0 for no fee, paid in same token)
5767
/// @param feeCollector The address to receive the fee (address(0) for no fee)
58-
/// @param permitV The permit signature v component
59-
/// @param permitR The permit signature r component
60-
/// @param permitS The permit signature s component
61-
/// @param sigV The intent signature v component
62-
/// @param sigR The intent signature r component
63-
/// @param sigS The intent signature s component
68+
/// @param description A description string for the intent
69+
/// @param permitSig The permit signature (v, r, s)
70+
/// @param intentSig The intent signature (v, r, s)
6471
function depositToIntentWithPermit(
6572
address user,
6673
address token,
@@ -71,12 +78,9 @@ interface ITrailsIntentEntrypoint {
7178
uint256 nonce,
7279
uint256 feeAmount,
7380
address feeCollector,
74-
uint8 permitV,
75-
bytes32 permitR,
76-
bytes32 permitS,
77-
uint8 sigV,
78-
bytes32 sigR,
79-
bytes32 sigS
81+
string calldata description,
82+
Signature calldata permitSig,
83+
Signature calldata intentSig
8084
) external;
8185

8286
/// @notice Deposit tokens to an intent address (requires prior approval)
@@ -88,9 +92,8 @@ interface ITrailsIntentEntrypoint {
8892
/// @param nonce The nonce for this user
8993
/// @param feeAmount The amount of fee to pay (0 for no fee, paid in same token)
9094
/// @param feeCollector The address to receive the fee (address(0) for no fee)
91-
/// @param sigV The intent signature v component
92-
/// @param sigR The intent signature r component
93-
/// @param sigS The intent signature s component
95+
/// @param description A description string for the intent
96+
/// @param intentSig The intent signature (v, r, s)
9497
function depositToIntent(
9598
address user,
9699
address token,
@@ -100,8 +103,7 @@ interface ITrailsIntentEntrypoint {
100103
uint256 nonce,
101104
uint256 feeAmount,
102105
address feeCollector,
103-
uint8 sigV,
104-
bytes32 sigR,
105-
bytes32 sigS
106+
string calldata description,
107+
Signature calldata intentSig
106108
) external;
107109
}

src/interfaces/ITrailsRouter.sol

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ interface ITrailsRouter is IDelegatedExtension {
4242
/// @return returnResults The result of the execution.
4343
function execute(bytes calldata data) external payable returns (IMulticall3.Result[] memory returnResults);
4444

45-
/// @notice Pull ERC20 from msg.sender, then delegatecall into Multicall3.
46-
/// @dev Requires prior approval to this router.
45+
/// @notice Pull tokens from msg.sender, then delegatecall into Multicall3.
46+
/// @dev For ERC20: pulls entire balance and requires prior approval. For ETH: uses msg.value.
4747
/// @param token The ERC20 token to pull, or address(0) for ETH.
4848
/// @param data The calldata for Multicall3.
4949
/// @return returnResults The result of the execution.
@@ -52,8 +52,8 @@ interface ITrailsRouter is IDelegatedExtension {
5252
payable
5353
returns (IMulticall3.Result[] memory returnResults);
5454

55-
/// @notice Pull specific amount of ERC20 from msg.sender, then delegatecall into Multicall3.
56-
/// @dev Requires prior approval to this router.
55+
/// @notice Pull specific amount of tokens from msg.sender, then delegatecall into Multicall3.
56+
/// @dev For ERC20: requires prior approval. For ETH: requires msg.value.
5757
/// @param token The ERC20 token to pull, or address(0) for ETH.
5858
/// @param amount The amount to pull.
5959
/// @param data The calldata for Multicall3.
@@ -68,7 +68,7 @@ interface ITrailsRouter is IDelegatedExtension {
6868
// ---------------------------------------------------------------------
6969

7070
/// @notice Sweeps tokens from msg.sender and calls target with modified calldata.
71-
/// @dev For regular calls (not delegatecall). Transfers tokens from msg.sender to this contract first.
71+
/// @dev Transfers tokens from msg.sender to this contract first.
7272
/// @param token The ERC-20 token to sweep, or address(0) for ETH.
7373
/// @param target The address to call with modified calldata.
7474
/// @param callData The original calldata (must include a 32-byte placeholder).
@@ -83,7 +83,6 @@ interface ITrailsRouter is IDelegatedExtension {
8383
) external payable;
8484

8585
/// @notice Injects balance and calls target (for delegatecall context).
86-
/// @dev For delegatecalls from Sequence wallets. Reads balance from address(this).
8786
/// @param token The ERC-20 token to sweep, or address(0) for ETH.
8887
/// @param target The address to call with modified calldata.
8988
/// @param callData The original calldata (must include a 32-byte placeholder).
@@ -108,8 +107,8 @@ interface ITrailsRouter is IDelegatedExtension {
108107
// Sweeper
109108
// ---------------------------------------------------------------------
110109

111-
/// @notice Approves the sweeper if ERC20, then sweeps the entire balance to recipient.
112-
/// @dev For delegatecall context. Approval is set for `SELF` on the wallet.
110+
/// @notice Sweeps the entire balance to recipient.
111+
/// @dev For delegatecall context. Transfers all tokens/ETH held by the wallet to the recipient.
113112
/// @param token The address of the token to sweep. Use address(0) for the native token.
114113
/// @param recipient The address to send the swept tokens to.
115114
function sweep(address token, address recipient) external payable;

0 commit comments

Comments
 (0)