From eeaaefd849f87bb6abe98a234d6d75d54ef7338d Mon Sep 17 00:00:00 2001 From: cucupac <46691282+cucupac@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:01:39 -0600 Subject: [PATCH] feat: add ERC712 permit to functions with transferFrom --- src/Position.sol | 31 ++++++++++ src/interfaces/IPosition.sol | 56 ++++++++++++++++++ src/interfaces/aave/IPool.sol | 16 +++--- src/interfaces/token/IERC20Permit.sol | 83 +++++++++++++++++++++++++++ src/services/DebtService.sol | 47 +++++++++++++++ 5 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 src/interfaces/token/IERC20Permit.sol diff --git a/src/Position.sol b/src/Position.sol index 44010af2..426967d8 100644 --- a/src/Position.sol +++ b/src/Position.sol @@ -6,6 +6,7 @@ import { DebtService } from "src/services/DebtService.sol"; import { SwapService } from "src/services/SwapService.sol"; import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; +import { IERC20Permit } from "src/interfaces/token/IERC20Permit.sol"; import { IFeeCollector } from "src/interfaces/IFeeCollector.sol"; /// @title Position @@ -62,6 +63,36 @@ contract Position is DebtService, SwapService { emit Short(cAmtNet, dAmt, bAmt); } + /** + * @notice Adds to this contract's short position with permit, obviating the need for a separate approve tx. + * @param _cAmt The amount of collateral to be supplied for this transaction-specific loan (units: C_DECIMALS). + * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). + * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. + * @param _poolFee The fee of the Uniswap pool. + * @param _client The address, controlled by client operators, for receiving protocol fees (use address(0) if no client). + * @param _deadline The deadline timestamp that the permit is valid. + * @param _v The V parameter of ERC712 permit signature. + * @param _r The R parameter of ERC712 permit signature. + * @param _s The S parameter of ERC712 permit signature. + */ + function shortWithPermit( + uint256 _cAmt, + uint256 _ltv, + uint256 _swapAmtOutMin, + uint24 _poolFee, + address _client, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public payable onlyOwner { + // 1. Approve with permit + IERC20WithPermit(C_TOKEN).permit(msg.sender, address(this), _cAmt, _deadline, _v, _r, _s); + + // 2. Short + short(_cAmt, _ltv, _swapAmtOutMin, _poolFee, _client); + } + /** * @notice Fully closes the short position. * @param _poolFee The fee of the Uniswap pool. diff --git a/src/interfaces/IPosition.sol b/src/interfaces/IPosition.sol index e88114d9..d37eed73 100644 --- a/src/interfaces/IPosition.sol +++ b/src/interfaces/IPosition.sol @@ -42,6 +42,30 @@ interface IPosition { external payable; + /** + * @notice Adds to this contract's short position with permit, obviating the need for a separate approve tx. + * @param _cAmt The amount of collateral to be supplied for this transaction-specific loan (units: C_DECIMALS). + * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). + * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. + * @param _poolFee The fee of the Uniswap pool. + * @param _client The address, controlled by client operators, for receiving protocol fees (use address(0) if no client). + * @param _deadline The deadline timestamp that the permit is valid. + * @param _v The V parameter of ERC712 permit signature. + * @param _r The R parameter of ERC712 permit signature. + * @param _s The S parameter of ERC712 permit signature. + */ + function shortWithPermit( + uint256 _cAmt, + uint256 _ltv, + uint256 _swapAmtOutMin, + uint24 _poolFee, + address _client, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external payable; + /** * @notice Fully closes the short position. * @param _poolFee The fee of the Uniswap pool. @@ -59,6 +83,18 @@ interface IPosition { */ function addCollateral(uint256 _cAmt) external payable; + /** + * @notice Increases the collateral amount for this contract's loan with permit, obviating the need for a separate approve tx. + * @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS). + * @param _deadline The deadline timestamp that the permit is valid. + * @param _v The V parameter of ERC712 permit signature. + * @param _r The R parameter of ERC712 permit signature. + * @param _s The S parameter of ERC712 permit signature. + */ + function addCollateralWithPermit(uint256 _cAmt, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) + external + payable; + /** * @notice Repays any outstanding debt to Aave and transfers remaining collateral from Aave to owner. * @param _dAmt The amount of debt token to repay to Aave (units: D_DECIMALS). @@ -67,6 +103,26 @@ interface IPosition { */ function repayAfterClose(uint256 _dAmt, uint256 _withdrawBuffer) external payable; + /** + * @notice Repays any outstanding debt to Aave and transfers remaining collateral from Aave to owner, + * with permit, obviating the need for a separate approve tx. + * @param _dAmt The amount of debt token to repay to Aave (units: D_DECIMALS). + * To pay off entire debt, _dAmt = debtOwed + smallBuffer (to account for interest). + * @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (default = 100_000, units: 8 decimals). + * @param _deadline The deadline timestamp that the permit is valid. + * @param _v The V parameter of ERC712 permit signature. + * @param _r The R parameter of ERC712 permit signature. + * @param _s The S parameter of ERC712 permit signature. + */ + function repayAfterCloseWithPermit( + uint256 _dAmt, + uint256 _withdrawBuffer, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external payable; + /* **************************************************************************** ** ** ADMIN FUNCTIONS diff --git a/src/interfaces/aave/IPool.sol b/src/interfaces/aave/IPool.sol index 2c513840..5116053f 100644 --- a/src/interfaces/aave/IPool.sol +++ b/src/interfaces/aave/IPool.sol @@ -254,12 +254,12 @@ interface IPool { * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens * is a different wallet - * @param deadline The deadline timestamp that the permit is valid + * @param deadline The deadline timestamp that the permit is valid. * @param referralCode Code used to register the integrator originating the operation, for potential rewards. * 0 if the action is executed directly by the user, without any middle-man - * @param permitV The V parameter of ERC712 permit sig - * @param permitR The R parameter of ERC712 permit sig - * @param permitS The S parameter of ERC712 permit sig + * @param permitV The V parameter of ERC712 permit signature. + * @param permitR The R parameter of ERC712 permit signature. + * @param permitS The S parameter of ERC712 permit signature. * */ function supplyWithPermit( @@ -333,10 +333,10 @@ interface IPool { * @param onBehalfOf Address of the user who will get his debt reduced/removed. Should be the address of the * user calling the function if he wants to reduce/remove his own debt, or the address of any other * other borrower whose debt should be removed - * @param deadline The deadline timestamp that the permit is valid - * @param permitV The V parameter of ERC712 permit sig - * @param permitR The R parameter of ERC712 permit sig - * @param permitS The S parameter of ERC712 permit sig + * @param deadline The deadline timestamp that the permit is valid. + * @param permitV The V parameter of ERC712 permit signature. + * @param permitR The R parameter of ERC712 permit signature. + * @param permitS The S parameter of ERC712 permit signature. * @return The final amount repaid * */ diff --git a/src/interfaces/token/IERC20Permit.sol b/src/interfaces/token/IERC20Permit.sol new file mode 100644 index 00000000..5d4fb8cb --- /dev/null +++ b/src/interfaces/token/IERC20Permit.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol) + +pragma solidity ^0.8.21; + +/** + * @dev Interface of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * ==== Security Considerations + * + * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature + * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be + * considered as an intention to spend the allowance in any specific way. The second is that because permits have + * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should + * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be + * generally recommended is: + * + * ```solidity + * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { + * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + * doThing(..., value); + * } + * + * function doThing(..., uint256 value) public { + * token.safeTransferFrom(msg.sender, address(this), value); + * ... + * } + * ``` + * + * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of + * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also + * {SafeERC20-safeTransferFrom}). + * + * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so + * contracts should have entry points that don't rely on permit. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/src/services/DebtService.sol b/src/services/DebtService.sol index c6102d0c..e93c6d8d 100644 --- a/src/services/DebtService.sol +++ b/src/services/DebtService.sol @@ -6,6 +6,7 @@ import { PositionAdmin } from "src/PositionAdmin.sol"; import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol"; import { IPool } from "src/interfaces/aave/IPool.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; +import { IERC20 } from "src/interfaces/token/IERC20.sol"; import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; import { IERC20Metadata } from "src/interfaces/token/IERC20Metadata.sol"; @@ -125,6 +126,26 @@ contract DebtService is PositionAdmin { IPool(AAVE_POOL).supply(C_TOKEN, _cAmt, address(this), 0); } + /** + * @notice Increases the collateral amount for this contract's loan with permit, obviating the need for a separate approve tx. + * @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS). + * @param _deadline The deadline timestamp that the permit is valid. + * @param _v The V parameter of ERC712 permit signature. + * @param _r The R parameter of ERC712 permit signature. + * @param _s The S parameter of ERC712 permit signature. + */ + function addCollateralWithPermit(uint256 _cAmt, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) + public + payable + onlyOwner + { + // 1. Approve with permit + IERC20WithPermit(C_TOKEN).permit(msg.sender, address(this), _cAmt, _deadline, _v, _r, _s); + + // 2. Add Collateral + addCollateral(_cAmt); + } + /** * @notice Repays any outstanding debt to Aave and transfers remaining collateral from Aave to owner. * @param _dAmt The amount of debt token to repay to Aave (units: D_DECIMALS). @@ -138,4 +159,30 @@ contract DebtService is PositionAdmin { _withdraw(OWNER, _withdrawBuffer); } + + /** + * @notice Repays any outstanding debt to Aave and transfers remaining collateral from Aave to owner, + * with permit, obviating the need for a separate approve tx. + * @param _dAmt The amount of debt token to repay to Aave (units: D_DECIMALS). + * To pay off entire debt, _dAmt = debtOwed + smallBuffer (to account for interest). + * @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (default = 100_000, units: 8 decimals). + * @param _deadline The deadline timestamp that the permit is valid. + * @param _v The V parameter of ERC712 permit signature. + * @param _r The R parameter of ERC712 permit signature. + * @param _s The S parameter of ERC712 permit signature. + */ + function repayAfterCloseWithPermit( + uint256 _dAmt, + uint256 _withdrawBuffer, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public payable onlyOwner { + // 1. Approve with permit + IERC20WithPermit(D_TOKEN).permit(msg.sender, address(this), _dAmt, _deadline, _v, _r, _s); + + // 2. Repay + repayAfterClose(_dAmt, _withdrawBuffer); + } }