diff --git a/foundry.toml b/foundry.toml index 2847118..8dcd3af 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,4 +9,4 @@ via_ir = true optimizer = true optimizer-runs = 2000 -rpc_endpoints = { mainnet = "https://rpc.ankr.com/eth", holesky = "https://rpc.ankr.com/eth_holesky" } +rpc_endpoints = { mainnet = "https://eth.drpc.org", holesky = "https://rpc.ankr.com/eth_holesky" } diff --git a/src/@permit2/interfaces/ISignatureTransfer.sol b/src/@permit2/interfaces/ISignatureTransfer.sol new file mode 100644 index 0000000..bacf1fe --- /dev/null +++ b/src/@permit2/interfaces/ISignatureTransfer.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {IEIP712} from "./IEIP712.sol"; + +/// @title SignatureTransfer +/// @notice Handles ERC20 token transfers through signature based actions +/// @dev Requires user's token approval on the Permit2 contract +interface ISignatureTransfer is IEIP712 { + /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount + /// @param maxAmount The maximum amount a spender can request to transfer + error InvalidAmount(uint256 maxAmount); + + /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred + /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred + error LengthMismatch(); + + /// @notice Emits an event when the owner successfully invalidates an unordered nonce. + event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask); + + /// @notice The token and amount details for a transfer signed in the permit transfer signature + struct TokenPermissions { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + /// @notice The signed permit message for a single token transfer + struct PermitTransferFrom { + TokenPermissions permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice Specifies the recipient address and amount for batched transfers. + /// @dev Recipients and amounts correspond to the index of the signed token permissions array. + /// @dev Reverts if the requested amount is greater than the permitted signed amount. + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /// @notice Used to reconstruct the signed permit message for multiple token transfers + /// @dev Do not need to pass in spender address as it is required that it is msg.sender + /// @dev Note that a user still signs over a spender address + struct PermitBatchTransferFrom { + // the tokens and corresponding amounts permitted for a transfer + TokenPermissions[] permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection + /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order + /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce + /// @dev It returns a uint256 bitmap + /// @dev The index, or wordPosition is capped at type(uint248).max + function nonceBitmap(address, uint256) external view returns (uint256); + + /// @notice Transfers a token using a signed permit message + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param signature The signature to verify + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers a token using a signed permit message + /// @notice Includes extra data provided by the caller to verify signature over + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param signature The signature to verify + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @notice Includes extra data provided by the caller to verify signature over + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Invalidates the bits specified in mask for the bitmap at the word position + /// @dev The wordPos is maxed at type(uint248).max + /// @param wordPos A number to index the nonceBitmap at + /// @param mask A bitmap masked against msg.sender's current bitmap at the word position + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; +} diff --git a/src/@permit2/libraries/PermitHash.sol b/src/@permit2/libraries/PermitHash.sol new file mode 100644 index 0000000..cdc0172 --- /dev/null +++ b/src/@permit2/libraries/PermitHash.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {IAllowanceTransfer} from "../interfaces/IAllowanceTransfer.sol"; +import {ISignatureTransfer} from "../interfaces/ISignatureTransfer.sol"; + +library PermitHash { + bytes32 public constant _PERMIT_DETAILS_TYPEHASH = + keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"); + + bytes32 public constant _PERMIT_SINGLE_TYPEHASH = keccak256( + "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + + bytes32 public constant _PERMIT_BATCH_TYPEHASH = keccak256( + "PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + + bytes32 public constant _TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + + bytes32 public constant _PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + bytes32 public constant _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + string public constant _TOKEN_PERMISSIONS_TYPESTRING = "TokenPermissions(address token,uint256 amount)"; + + string public constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB = + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; + + string public constant _PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB = + "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,"; + + function hash(IAllowanceTransfer.PermitSingle memory permitSingle) internal pure returns (bytes32) { + bytes32 permitHash = _hashPermitDetails(permitSingle.details); + return + keccak256(abi.encode(_PERMIT_SINGLE_TYPEHASH, permitHash, permitSingle.spender, permitSingle.sigDeadline)); + } + + function hash(IAllowanceTransfer.PermitBatch memory permitBatch) internal pure returns (bytes32) { + uint256 numPermits = permitBatch.details.length; + bytes32[] memory permitHashes = new bytes32[](numPermits); + for (uint256 i = 0; i < numPermits; ++i) { + permitHashes[i] = _hashPermitDetails(permitBatch.details[i]); + } + return keccak256( + abi.encode( + _PERMIT_BATCH_TYPEHASH, + keccak256(abi.encodePacked(permitHashes)), + permitBatch.spender, + permitBatch.sigDeadline + ) + ); + } + + function hash(ISignatureTransfer.PermitTransferFrom memory permit) internal view returns (bytes32) { + bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted); + return keccak256( + abi.encode(_PERMIT_TRANSFER_FROM_TYPEHASH, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline) + ); + } + + function hash(ISignatureTransfer.PermitBatchTransferFrom memory permit) internal view returns (bytes32) { + uint256 numPermitted = permit.permitted.length; + bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted); + + for (uint256 i = 0; i < numPermitted; ++i) { + tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]); + } + + return keccak256( + abi.encode( + _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH, + keccak256(abi.encodePacked(tokenPermissionHashes)), + msg.sender, + permit.nonce, + permit.deadline + ) + ); + } + + function hashWithWitness( + ISignatureTransfer.PermitTransferFrom memory permit, + bytes32 witness, + string calldata witnessTypeString + ) internal view returns (bytes32) { + bytes32 typeHash = keccak256(abi.encodePacked(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, witnessTypeString)); + + bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted); + return keccak256(abi.encode(typeHash, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline, witness)); + } + + function hashWithWitness( + ISignatureTransfer.PermitBatchTransferFrom memory permit, + bytes32 witness, + string calldata witnessTypeString + ) internal view returns (bytes32) { + bytes32 typeHash = + keccak256(abi.encodePacked(_PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB, witnessTypeString)); + + uint256 numPermitted = permit.permitted.length; + bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted); + + for (uint256 i = 0; i < numPermitted; ++i) { + tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]); + } + + return keccak256( + abi.encode( + typeHash, + keccak256(abi.encodePacked(tokenPermissionHashes)), + msg.sender, + permit.nonce, + permit.deadline, + witness + ) + ); + } + + function _hashPermitDetails(IAllowanceTransfer.PermitDetails memory details) private pure returns (bytes32) { + return keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, details)); + } + + function _hashTokenPermissions(ISignatureTransfer.TokenPermissions memory permitted) + private + pure + returns (bytes32) + { + return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permitted)); + } +} diff --git a/src/@permit2/libraries/SignatureVerification.sol b/src/@permit2/libraries/SignatureVerification.sol new file mode 100644 index 0000000..d9a015e --- /dev/null +++ b/src/@permit2/libraries/SignatureVerification.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "../../@openzeppelin/contracts/interfaces/IERC1271.sol"; + +library SignatureVerification { + /// @notice Thrown when the passed in signature is not a valid length + error InvalidSignatureLength(); + + /// @notice Thrown when the recovered signer is equal to the zero address + error InvalidSignature(); + + /// @notice Thrown when the recovered signer does not equal the claimedSigner + error InvalidSigner(); + + /// @notice Thrown when the recovered contract signature is incorrect + error InvalidContractSignature(); + + bytes32 constant UPPER_BIT_MASK = (0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + + function verify(bytes calldata signature, bytes32 hash, address claimedSigner) internal view { + bytes32 r; + bytes32 s; + uint8 v; + + if (claimedSigner.code.length == 0) { + if (signature.length == 65) { + (r, s) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(signature[64]); + } else if (signature.length == 64) { + // EIP-2098 + bytes32 vs; + (r, vs) = abi.decode(signature, (bytes32, bytes32)); + s = vs & UPPER_BIT_MASK; + v = uint8(uint256(vs >> 255)) + 27; + } else { + revert InvalidSignatureLength(); + } + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) revert InvalidSignature(); + if (signer != claimedSigner) revert InvalidSigner(); + } else { + bytes4 magicValue = IERC1271(claimedSigner).isValidSignature(hash, signature); + if (magicValue != IERC1271.isValidSignature.selector) revert InvalidContractSignature(); + } + } +} diff --git a/src/mocks/IMorphoEthereumBundlerV2.sol b/src/mocks/IMorphoEthereumBundlerV2.sol new file mode 100644 index 0000000..8c8f0cb --- /dev/null +++ b/src/mocks/IMorphoEthereumBundlerV2.sol @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../@permit2/interfaces/IAllowanceTransfer.sol"; + +interface IMorphoEthereumBundlerV2 { + /// @notice Approves the given `amount` of `asset` from the initiator to be spent by `permitSingle.spender` via + /// Permit2 with the given `deadline` & EIP-712 `signature`. + /// @param permitSingle The `PermitSingle` struct. + /// @param signature The signature, serialized. + /// @param skipRevert Whether to avoid reverting the call in case the signature is frontrunned. + function approve2(IAllowanceTransfer.PermitSingle calldata permitSingle, bytes calldata signature, bool skipRevert) + external + payable; + + /// @notice Transfers the given `amount` of `asset` from the initiator to the bundler via Permit2. + /// @param asset The address of the ERC20 token to transfer. + /// @param amount The amount of `asset` to transfer from the initiator. Capped at the initiator's balance. + function transferFrom2(address asset, uint256 amount) external payable; + + /// @notice Deposits the given amount of `assets` on the given ERC4626 `vault`, on behalf of `receiver`. + /// @dev Initiator must have previously transferred their assets to the bundler. + /// @dev Assumes the given `vault` implements EIP-4626. + /// @param vault The address of the vault. + /// @param assets The amount of assets to deposit. Capped at the bundler's assets. + /// @param minShares The minimum amount of shares to mint in exchange for `assets`. This parameter is proportionally + /// scaled down in case there are fewer assets than `assets` on the bundler. + /// @param receiver The address to which shares will be minted. + function erc4626Deposit(address vault, uint256 assets, uint256 minShares, address receiver) + external + payable; + + /// @notice Executes a series of delegate calls to the contract itself. + /// @dev Locks the initiator so that the sender can uniquely be identified in callbacks. + /// @dev All functions delegatecalled must be `payable` if `msg.value` is non-zero. + function multicall(bytes[] memory data) external payable; +} diff --git a/src/p2pLendingProxy/P2pLendingProxy.sol b/src/p2pLendingProxy/P2pLendingProxy.sol index 62eddab..f67e4e3 100644 --- a/src/p2pLendingProxy/P2pLendingProxy.sol +++ b/src/p2pLendingProxy/P2pLendingProxy.sol @@ -3,12 +3,14 @@ pragma solidity 0.8.27; -import "../@permit2/interfaces/IAllowanceTransfer.sol"; -import "../@permit2/libraries/Permit2Lib.sol"; +import "../@openzeppelin/contracts/interfaces/IERC1271.sol"; import "../@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../@openzeppelin/contracts/utils/Address.sol"; import "../@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "../@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import "../@permit2/interfaces/IAllowanceTransfer.sol"; +import "../@permit2/libraries/Permit2Lib.sol"; +import "../@permit2/libraries/SignatureVerification.sol"; import "../p2pLendingProxyFactory/IP2pLendingProxyFactory.sol"; import "./IP2pLendingProxy.sol"; @@ -22,7 +24,7 @@ error P2pLendingProxy__NotFactoryCalled( IP2pLendingProxyFactory _actualFactory ); -contract P2pLendingProxy is ERC165, IP2pLendingProxy { +contract P2pLendingProxy is ERC165, IP2pLendingProxy, IERC1271 { IP2pLendingProxyFactory private immutable i_factory; @@ -40,12 +42,6 @@ contract P2pLendingProxy is ERC165, IP2pLendingProxy { constructor( address _factory ) { - if (!ERC165Checker.supportsInterface( - _factory, - type(IP2pLendingProxyFactory).interfaceId) - ) { - revert P2pLendingProxy__NotFactory(_factory); - } i_factory = IP2pLendingProxyFactory(_factory); } @@ -92,6 +88,12 @@ contract P2pLendingProxy is ERC165, IP2pLendingProxy { Address.functionCall(lendingProtocolAddress, lendingProtocolCalldata); } + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue) { + SignatureVerification.verify(signature, hash, s_client); + + return IERC1271.isValidSignature.selector; + } + /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { return interfaceId == type(IP2pLendingProxy).interfaceId || diff --git a/src/p2pLendingProxyFactory/P2pLendingProxyFactory.sol b/src/p2pLendingProxyFactory/P2pLendingProxyFactory.sol index 46e6231..33d9ccc 100644 --- a/src/p2pLendingProxyFactory/P2pLendingProxyFactory.sol +++ b/src/p2pLendingProxyFactory/P2pLendingProxyFactory.sol @@ -22,6 +22,9 @@ error P2pLendingProxyFactory__NotP2pOperatorCalled( address _msgSender, address _actualP2pOperator ); +error P2pLendingProxyFactory__P2pSignerSignatureExpired( + uint256 _p2pSignerSigDeadline +); contract P2pLendingProxyFactory is P2pLendingProxyFactoryStructs, ERC165, IP2pLendingProxyFactory { using SafeCast160 for uint256; @@ -49,6 +52,20 @@ contract P2pLendingProxyFactory is P2pLendingProxyFactoryStructs, ERC165, IP2pLe i_referenceP2pLendingProxy = new P2pLendingProxy(address(this)); s_p2pSigner = _p2pSigner; + s_p2pOperator = msg.sender; + } + + // TODO: add 2 step + function setP2pOperator( + address _newP2pOperator + ) external onlyP2pOperator { + s_p2pOperator = _newP2pOperator; + } + + function setP2pSigner( + address _newP2pSigner + ) external onlyP2pOperator { + s_p2pSigner = _newP2pSigner; } function setAllowedFunctionForContract( @@ -79,15 +96,20 @@ contract P2pLendingProxyFactory is P2pLendingProxyFactoryStructs, ERC165, IP2pLe external returns (address p2pLendingProxyAddress) { + if (block.timestamp > _p2pSignerSigDeadline) { + revert P2pLendingProxyFactory__P2pSignerSignatureExpired(_p2pSignerSigDeadline); + } + // verify P2P Signer signature bytes32 hash = getHashForP2pSigner( msg.sender, _clientBasisPoints, _p2pSignerSigDeadline ); + bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(hash); bool isValid = SignatureChecker.isValidSignatureNow( s_p2pSigner, - hash, + ethSignedMessageHash, _p2pSignerSignature ); if (!isValid) { diff --git a/test/MainnetIntegration.sol b/test/MainnetIntegration.sol new file mode 100644 index 0000000..bb364a0 --- /dev/null +++ b/test/MainnetIntegration.sol @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2024 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../src/mocks/IMorphoEthereumBundlerV2.sol"; +import "../src/p2pLendingProxyFactory/P2pLendingProxyFactory.sol"; +import "../src/p2pLendingProxyFactory/P2pLendingProxyFactoryStructs.sol"; +import "forge-std/Test.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import "forge-std/console2.sol"; +import {PermitHash} from "../src/@permit2/libraries/PermitHash.sol"; + + +contract MainnetIntegration is Test { + P2pLendingProxyFactory private factory; + + address private clientAddress; + uint256 private clientPrivateKey; + + address private p2pSignerAddress; + uint256 private p2pSignerPrivateKey; + + address private p2pOperatorAddress; + address private nobody; + + address constant MorphoEthereumBundlerV2 = 0x4095F064B8d3c3548A3bebfd0Bbfd04750E30077; + address constant USDT = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + // address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + + uint256 constant SigDeadline = 1734464723; + uint96 constant ClientBasisPoints = 8700; // 13% fee + + function setUp() public { + vm.createSelectFork("mainnet", 21308893); + + (clientAddress, clientPrivateKey) = makeAddrAndKey("client"); + (p2pSignerAddress, p2pSignerPrivateKey) = makeAddrAndKey("p2pSigner"); + p2pOperatorAddress = makeAddr("p2pOperator"); + nobody = makeAddr("nobody"); + + deal(USDT, clientAddress, 10000e18); + + vm.startPrank(p2pOperatorAddress); + factory = new P2pLendingProxyFactory(p2pSignerAddress); + vm.stopPrank(); + } + + function test__Mainnet() external { + // allowed calldata for factory + bytes4 multicallSelector = IMorphoEthereumBundlerV2.multicall.selector; + bytes memory allowedBytes = ""; + P2pLendingProxyFactoryStructs.Rule memory rule = P2pLendingProxyFactoryStructs.Rule({ + ruleType: P2pLendingProxyFactoryStructs.RuleType.AnyCalldata, + index: 0, + allowedBytes: allowedBytes + }); + P2pLendingProxyFactoryStructs.Rule[] memory rules = new P2pLendingProxyFactoryStructs.Rule[](1); + rules[0] = rule; + P2pLendingProxyFactoryStructs.AllowedCalldata memory allowedCalldata = P2pLendingProxyFactoryStructs.AllowedCalldata({ + functionType: P2pLendingProxyFactoryStructs.FunctionType.Deposit, + rules: rules + }); + + vm.startPrank(p2pOperatorAddress); + factory.setAllowedFunctionForContract( + MorphoEthereumBundlerV2, + multicallSelector, + allowedCalldata + ); + vm.stopPrank(); + + // morpho approve2 + IAllowanceTransfer.PermitDetails memory permitDetails = IAllowanceTransfer.PermitDetails({ + token: USDT, + amount: 10000000, + expiration: 281474976710655, + nonce: 0 + }); + IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ + details: permitDetails, + spender: MorphoEthereumBundlerV2, + sigDeadline: SigDeadline + }); + bytes32 permitSingleHash = PermitHash.hash(permitSingle); + (uint8 v0, bytes32 r0, bytes32 s0) = vm.sign(clientPrivateKey, permitSingleHash); + bytes memory signatureForApprove2 = abi.encodePacked(r0, s0, v0); + bytes memory approve2CallData = abi.encodeCall(IMorphoEthereumBundlerV2.approve2, ( + permitSingle, + signatureForApprove2, + true + )); + + // morpho multicall + bytes[] memory dataForMulticall = new bytes[](1); + dataForMulticall[0] = approve2CallData; + bytes memory multicallCallData = abi.encodeCall(IMorphoEthereumBundlerV2.multicall, (dataForMulticall)); + + // data for factory + address proxyAddress = factory.predictP2pLendingProxyAddress(clientAddress, ClientBasisPoints); + IAllowanceTransfer.PermitSingle memory permitSingleForP2pLendingProxy = IAllowanceTransfer.PermitSingle({ + details: permitDetails, + spender: proxyAddress, + sigDeadline: SigDeadline + }); + bytes32 permitSingleForP2pLendingProxyHash = PermitHash.hash(permitSingleForP2pLendingProxy); + (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(clientPrivateKey, permitSingleForP2pLendingProxyHash); + bytes memory permit2SignatureForP2pLendingProxy = abi.encodePacked(r1, s1, v1); + + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + clientAddress, + ClientBasisPoints, + SigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(p2pSignerPrivateKey, ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + + vm.startPrank(clientAddress); + IERC20(USDT).approve(address(Permit2Lib.PERMIT2), type(uint256).max); + factory.deposit( + MorphoEthereumBundlerV2, + multicallCallData, + permitSingleForP2pLendingProxy, + permit2SignatureForP2pLendingProxy, + + ClientBasisPoints, + SigDeadline, + p2pSignerSignature + ); + vm.stopPrank(); + } +} \ No newline at end of file