Skip to content

Commit 17958e9

Browse files
committed
add AdminWithdrawManager and cleanup withdraw functions & param hash verification
1 parent e8e2992 commit 17958e9

13 files changed

+560
-153
lines changed

contracts/interfaces/ICounterfactualDeposit.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,34 @@ interface ICounterfactualDeposit {
1919
/// @dev EIP-712 signature deadline has passed. SpokePool only.
2020
error SignatureExpired();
2121

22+
/// @notice Emitted when the admin withdraws tokens from the clone.
2223
event AdminWithdraw(address indexed token, address indexed to, uint256 amount);
24+
/// @notice Emitted when the user withdraws tokens from the clone.
2325
event UserWithdraw(address indexed token, address indexed to, uint256 amount);
26+
27+
/**
28+
* @notice Admin withdraw to an arbitrary recipient.
29+
* @param params ABI-encoded route parameters (verified against stored hash).
30+
* @param token ERC20 token to withdraw, or NATIVE_ASSET for native ETH.
31+
* @param to Recipient of the withdrawn tokens.
32+
* @param amount Amount to withdraw.
33+
*/
34+
function adminWithdraw(bytes calldata params, address token, address to, uint256 amount) external;
35+
36+
/**
37+
* @notice Admin withdraw that always sends to the clone's userWithdrawAddress.
38+
* @param params ABI-encoded route parameters (verified against stored hash).
39+
* @param token ERC20 token to withdraw, or NATIVE_ASSET for native ETH.
40+
* @param amount Amount to withdraw.
41+
*/
42+
function adminWithdrawToUser(bytes calldata params, address token, uint256 amount) external;
43+
44+
/**
45+
* @notice User withdraw (escape hatch before execution).
46+
* @param params ABI-encoded route parameters (verified against stored hash).
47+
* @param token ERC20 token to withdraw, or NATIVE_ASSET for native ETH.
48+
* @param to Recipient of the withdrawn tokens.
49+
* @param amount Amount to withdraw.
50+
*/
51+
function userWithdraw(bytes calldata params, address token, address to, uint256 amount) external;
2452
}

contracts/interfaces/ICounterfactualDepositFactory.sol

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,70 @@ pragma solidity ^0.8.0;
88
* each implementation defines its own immutables struct, and the factory stores only the hash.
99
*/
1010
interface ICounterfactualDepositFactory {
11+
/// @notice Emitted when a new clone is deployed.
1112
event DepositAddressCreated(
1213
address indexed depositAddress,
1314
address indexed counterfactualDepositImplementation,
1415
bytes32 indexed paramsHash,
1516
bytes32 salt
1617
);
1718

19+
/**
20+
* @notice Predicts the deterministic address of a clone before deployment.
21+
* @param counterfactualDepositImplementation Implementation contract address.
22+
* @param paramsHash keccak256 hash of the ABI-encoded route parameters.
23+
* @param salt Unique salt for address generation.
24+
* @return Predicted address.
25+
*/
1826
function predictDepositAddress(
1927
address counterfactualDepositImplementation,
2028
bytes32 paramsHash,
2129
bytes32 salt
2230
) external view returns (address);
2331

32+
/**
33+
* @notice Deploys a counterfactual deposit clone via CREATE2.
34+
* @param counterfactualDepositImplementation Implementation contract address.
35+
* @param paramsHash keccak256 hash of the ABI-encoded route parameters.
36+
* @param salt Unique salt for address generation.
37+
* @return depositAddress Address of deployed clone.
38+
*/
2439
function deploy(
2540
address counterfactualDepositImplementation,
2641
bytes32 paramsHash,
2742
bytes32 salt
28-
) external returns (address);
43+
) external returns (address depositAddress);
2944

45+
/**
46+
* @notice Forwards calldata to a deployed clone.
47+
* @param depositAddress Address of the deployed clone.
48+
* @param executeCalldata Calldata to forward (e.g. abi.encodeCall of executeDeposit).
49+
*/
3050
function execute(address depositAddress, bytes calldata executeCalldata) external payable;
3151

52+
/**
53+
* @notice Deploys and executes a deposit in one transaction.
54+
* @param counterfactualDepositImplementation Implementation contract address.
55+
* @param paramsHash keccak256 hash of the ABI-encoded route parameters.
56+
* @param salt Unique salt for address generation.
57+
* @param executeCalldata Calldata to forward to the clone.
58+
* @return depositAddress Address of deployed clone.
59+
*/
3260
function deployAndExecute(
3361
address counterfactualDepositImplementation,
3462
bytes32 paramsHash,
3563
bytes32 salt,
3664
bytes calldata executeCalldata
3765
) external payable returns (address depositAddress);
3866

67+
/**
68+
* @notice Deploys (if needed) and executes a deposit in one transaction.
69+
* @param counterfactualDepositImplementation Implementation contract address.
70+
* @param paramsHash keccak256 hash of the ABI-encoded route parameters.
71+
* @param salt Unique salt for address generation.
72+
* @param executeCalldata Calldata to forward to the clone.
73+
* @return depositAddress Address of deployed clone.
74+
*/
3975
function deployIfNeededAndExecute(
4076
address counterfactualDepositImplementation,
4177
bytes32 paramsHash,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
5+
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6+
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7+
import { ICounterfactualDeposit } from "../../interfaces/ICounterfactualDeposit.sol";
8+
9+
/**
10+
* @title AdminWithdrawManager
11+
* @notice Manages admin withdrawals from counterfactual deposit clones via two paths:
12+
* 1. Direct withdraw — trusted `directWithdrawer` specifies any recipient
13+
* 2. Signed withdraw — anyone can trigger with a `signer` signature; recipient is always the clone's userWithdrawAddress
14+
* @dev Set this contract's address as `adminWithdrawAddress` on clones.
15+
*/
16+
contract AdminWithdrawManager is Ownable, EIP712 {
17+
event DirectWithdrawerUpdated(address indexed directWithdrawer);
18+
event SignerUpdated(address indexed signer);
19+
20+
error Unauthorized();
21+
error InvalidSignature();
22+
error SignatureExpired();
23+
24+
bytes32 public constant SIGNED_WITHDRAW_TYPEHASH =
25+
keccak256("SignedWithdraw(address depositAddress,address token,uint256 amount,uint256 deadline)");
26+
27+
address public directWithdrawer;
28+
address public signer;
29+
30+
/**
31+
* @param _owner Contract owner (can update directWithdrawer and signer).
32+
* @param _directWithdrawer Initial direct withdrawer address.
33+
* @param _signer Initial signer address for signed withdrawals.
34+
*/
35+
constructor(
36+
address _owner,
37+
address _directWithdrawer,
38+
address _signer
39+
) Ownable(_owner) EIP712("AdminWithdrawManager", "v1.0.0") {
40+
directWithdrawer = _directWithdrawer;
41+
signer = _signer;
42+
}
43+
44+
/**
45+
* @notice Direct withdraw — forwards raw calldata to the deposit clone.
46+
* @dev Only callable by `directWithdrawer`. Caller encodes the implementation-specific adminWithdraw call.
47+
* @param depositAddress Address of the deployed clone.
48+
* @param adminWithdrawCalldata Encoded call to adminWithdraw on the clone.
49+
*/
50+
function directWithdraw(address depositAddress, bytes calldata adminWithdrawCalldata) external {
51+
if (msg.sender != directWithdrawer) revert Unauthorized();
52+
(bool success, bytes memory returnData) = depositAddress.call(adminWithdrawCalldata);
53+
if (!success) {
54+
assembly {
55+
revert(add(returnData, 32), mload(returnData))
56+
}
57+
}
58+
}
59+
60+
/**
61+
* @notice Signed withdraw to user — anyone can trigger with a valid signature from `signer`.
62+
* @dev Recipient is always the clone's `userWithdrawAddress` (enforced by adminWithdrawToUser).
63+
* @param depositAddress Address of the deployed clone.
64+
* @param paramsBytes ABI-encoded route parameters (passed to adminWithdrawToUser).
65+
* @param token ERC20 token to withdraw, or NATIVE_ASSET for native ETH.
66+
* @param amount Amount to withdraw.
67+
* @param deadline Timestamp after which the signature is no longer valid.
68+
* @param signature EIP-712 signature from `signer`.
69+
*/
70+
function signedWithdrawToUser(
71+
address depositAddress,
72+
bytes calldata paramsBytes,
73+
address token,
74+
uint256 amount,
75+
uint256 deadline,
76+
bytes calldata signature
77+
) external {
78+
if (block.timestamp > deadline) revert SignatureExpired();
79+
80+
bytes32 structHash = keccak256(abi.encode(SIGNED_WITHDRAW_TYPEHASH, depositAddress, token, amount, deadline));
81+
if (ECDSA.recover(_hashTypedDataV4(structHash), signature) != signer) revert InvalidSignature();
82+
83+
ICounterfactualDeposit(depositAddress).adminWithdrawToUser(paramsBytes, token, amount);
84+
}
85+
86+
/**
87+
* @notice Updates the direct withdrawer address.
88+
* @param _directWithdrawer New direct withdrawer address.
89+
*/
90+
function setDirectWithdrawer(address _directWithdrawer) external onlyOwner {
91+
directWithdrawer = _directWithdrawer;
92+
emit DirectWithdrawerUpdated(_directWithdrawer);
93+
}
94+
95+
/**
96+
* @notice Updates the signer address used for signed withdrawals.
97+
* @param _signer New signer address.
98+
*/
99+
function setSigner(address _signer) external onlyOwner {
100+
signer = _signer;
101+
emit SignerUpdated(_signer);
102+
}
103+
}

contracts/periphery/counterfactual/CounterfactualDepositBase.sol

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,87 @@ abstract contract CounterfactualDepositBase is ICounterfactualDeposit {
1919
/// @notice Sentinel address representing native ETH in withdraw calls.
2020
address public constant NATIVE_ASSET = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
2121

22-
/// @dev Reads the stored params hash from the clone's appended immutable args and compares.
23-
/// @param paramsHash keccak256 hash of the caller-supplied route parameters.
24-
function _verifyParamsHash(bytes32 paramsHash) internal view {
22+
/// @dev Verifies caller-supplied params hash against the clone's stored hash.
23+
modifier verifyParamsHash(bytes32 paramsHash) {
2524
bytes32 storedHash = abi.decode(Clones.fetchCloneArgs(address(this)), (bytes32));
2625
if (paramsHash != storedHash) revert InvalidParamsHash();
26+
_;
2727
}
2828

2929
/**
3030
* @notice Allows the admin to withdraw any token from this clone (e.g. recovery of stuck funds).
31-
* @param adminWithdrawAddress Authorized admin address.
31+
* @param params ABI-encoded route parameters (verified against stored hash).
3232
* @param token ERC20 token to withdraw, or NATIVE_ASSET for native ETH.
3333
* @param to Recipient of the withdrawn tokens.
3434
* @param amount Amount to withdraw.
3535
*/
36-
function _adminWithdraw(address adminWithdrawAddress, address token, address to, uint256 amount) internal {
37-
if (msg.sender != adminWithdrawAddress) revert Unauthorized();
36+
function adminWithdraw(
37+
bytes calldata params,
38+
address token,
39+
address to,
40+
uint256 amount
41+
) external verifyParamsHash(keccak256(params)) {
42+
if (msg.sender != _getAdminWithdrawAddress(params)) revert Unauthorized();
43+
_transferOut(token, to, amount);
44+
emit AdminWithdraw(token, to, amount);
45+
}
46+
47+
/**
48+
* @notice Admin withdraw that always sends to the clone's userWithdrawAddress.
49+
* @dev Used by AdminWithdrawManager.signedWithdrawToUser so the recipient is enforced on-chain.
50+
* @param params ABI-encoded route parameters (verified against stored hash).
51+
* @param token ERC20 token to withdraw, or NATIVE_ASSET for native ETH.
52+
* @param amount Amount to withdraw.
53+
*/
54+
function adminWithdrawToUser(
55+
bytes calldata params,
56+
address token,
57+
uint256 amount
58+
) external verifyParamsHash(keccak256(params)) {
59+
if (msg.sender != _getAdminWithdrawAddress(params)) revert Unauthorized();
60+
address to = _getUserWithdrawAddress(params);
3861
_transferOut(token, to, amount);
3962
emit AdminWithdraw(token, to, amount);
4063
}
4164

4265
/**
4366
* @notice Allows the user to withdraw tokens before execution (escape hatch).
44-
* @param userWithdrawAddress Authorized user address.
67+
* @param params ABI-encoded route parameters (verified against stored hash).
4568
* @param token ERC20 token to withdraw, or NATIVE_ASSET for native ETH.
4669
* @param to Recipient of the withdrawn tokens.
4770
* @param amount Amount to withdraw.
4871
*/
49-
function _userWithdraw(address userWithdrawAddress, address token, address to, uint256 amount) internal {
50-
if (msg.sender != userWithdrawAddress) revert Unauthorized();
72+
function userWithdraw(
73+
bytes calldata params,
74+
address token,
75+
address to,
76+
uint256 amount
77+
) external verifyParamsHash(keccak256(params)) {
78+
if (msg.sender != _getUserWithdrawAddress(params)) revert Unauthorized();
5179
_transferOut(token, to, amount);
5280
emit UserWithdraw(token, to, amount);
5381
}
5482

55-
/// @dev Transfers native ETH (token == NATIVE_ASSET) or ERC20 tokens.
83+
/**
84+
* @dev Extracts the user withdraw address from implementation-specific params.
85+
* @param params ABI-encoded route parameters.
86+
* @return User withdraw address.
87+
*/
88+
function _getUserWithdrawAddress(bytes calldata params) internal pure virtual returns (address);
89+
90+
/**
91+
* @dev Extracts the admin withdraw address from implementation-specific params.
92+
* @param params ABI-encoded route parameters.
93+
* @return Admin withdraw address.
94+
*/
95+
function _getAdminWithdrawAddress(bytes calldata params) internal pure virtual returns (address);
96+
97+
/**
98+
* @dev Transfers native ETH (token == NATIVE_ASSET) or ERC20 tokens.
99+
* @param token ERC20 token address, or NATIVE_ASSET for native ETH.
100+
* @param to Recipient address.
101+
* @param amount Amount to transfer.
102+
*/
56103
function _transferOut(address token, address to, uint256 amount) internal {
57104
if (token == NATIVE_ASSET) {
58105
(bool success, ) = to.call{ value: amount }("");

contracts/periphery/counterfactual/CounterfactualDepositCCTP.sol

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,10 @@ contract CounterfactualDepositCCTP is CounterfactualDepositBase {
6969
/// @notice CCTP source domain ID for this chain
7070
uint32 public immutable sourceDomain;
7171

72-
/// @dev Hashes caller-supplied params and checks against the clone's stored hash.
73-
modifier verifyParams(CCTPImmutables memory params) {
74-
_verifyParamsHash(keccak256(abi.encode(params)));
75-
_;
76-
}
77-
72+
/**
73+
* @param _srcPeriphery SponsoredCCTPSrcPeriphery contract address.
74+
* @param _sourceDomain CCTP source domain ID for this chain.
75+
*/
7876
constructor(address _srcPeriphery, uint32 _sourceDomain) {
7977
srcPeriphery = _srcPeriphery;
8078
sourceDomain = _sourceDomain;
@@ -96,7 +94,7 @@ contract CounterfactualDepositCCTP is CounterfactualDepositBase {
9694
bytes32 nonce,
9795
uint256 cctpDeadline,
9896
bytes calldata signature
99-
) external verifyParams(params) {
97+
) external verifyParamsHash(keccak256(abi.encode(params))) {
10098
address inputToken = address(uint160(uint256(params.depositParams.burnToken)));
10199

102100
// transfer execution fee to execution fee recipient
@@ -135,35 +133,13 @@ contract CounterfactualDepositCCTP is CounterfactualDepositBase {
135133
emit CCTPDepositExecuted(amount, executionFeeRecipient, nonce, cctpDeadline);
136134
}
137135

138-
/**
139-
* @notice Allows admin to withdraw any token from this clone.
140-
* @param params Route parameters (verified against stored hash).
141-
* @param token ERC20 token to withdraw.
142-
* @param to Recipient of the withdrawn tokens.
143-
* @param amount Amount to withdraw.
144-
*/
145-
function adminWithdraw(
146-
CCTPImmutables memory params,
147-
address token,
148-
address to,
149-
uint256 amount
150-
) external verifyParams(params) {
151-
_adminWithdraw(params.executionParams.adminWithdrawAddress, token, to, amount);
136+
/// @inheritdoc CounterfactualDepositBase
137+
function _getUserWithdrawAddress(bytes calldata params) internal pure override returns (address) {
138+
return abi.decode(params, (CCTPImmutables)).executionParams.userWithdrawAddress;
152139
}
153140

154-
/**
155-
* @notice Allows user to withdraw tokens before execution.
156-
* @param params Route parameters (verified against stored hash).
157-
* @param token ERC20 token to withdraw.
158-
* @param to Recipient of the withdrawn tokens.
159-
* @param amount Amount to withdraw.
160-
*/
161-
function userWithdraw(
162-
CCTPImmutables memory params,
163-
address token,
164-
address to,
165-
uint256 amount
166-
) external verifyParams(params) {
167-
_userWithdraw(params.executionParams.userWithdrawAddress, token, to, amount);
141+
/// @inheritdoc CounterfactualDepositBase
142+
function _getAdminWithdrawAddress(bytes calldata params) internal pure override returns (address) {
143+
return abi.decode(params, (CCTPImmutables)).executionParams.adminWithdrawAddress;
168144
}
169145
}

contracts/periphery/counterfactual/CounterfactualDepositFactory.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ contract CounterfactualDepositFactory is ICounterfactualDepositFactory {
9999
);
100100
}
101101

102-
/// @dev Forwards calldata to a clone, bubbling up any revert.
102+
/**
103+
* @dev Forwards calldata to a clone, bubbling up any revert.
104+
* @param depositAddress Address of the deployed clone.
105+
* @param executeCalldata Calldata to forward.
106+
*/
103107
function _execute(address depositAddress, bytes calldata executeCalldata) internal {
104108
(bool success, bytes memory returnData) = depositAddress.call{ value: msg.value }(executeCalldata);
105109
if (!success) {

0 commit comments

Comments
 (0)