Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ERC712 permit to functions with transferFrom #9

Merged
merged 3 commits into from
Jan 27, 2024
Merged
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
25 changes: 6 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,21 @@ On-chain shorting via Aave and Uniswap.

## Principles

1. Immutable
2. No Governance
3. No Admin Keys
The following outlines principles for core protocol funcitonality.

1. Immutable.
2. No Governance.
3. No Admin Keys.

## To-Do

Logic:

- [x] Make `FEE_COLLECTOR` an immutable variable in Position.sol and PositionFactory.sol
- [x] Add `owner()` and `clientRate()` to IFeeCollector
- All caught up!🙂

Tests:

- [ ] Separate integration tests from unit tests (separate PR)
- [x] testFuzz_CollectFeesWithClient
- [x] testFuzz_CollectFeesNoClient
- [x] testFuzz_CollectFeesWithClientIntegrated
- [x] testFuzz_CollectFeesNoClientIntegrated
- [x] testFuzz_ClientWithdraw
- [x] testFuzz_SetClientRate
- [x] testFuzz_CannotSetClientRateOutOfRange
- [x] testFuzz_CannotSetClientRateUnauthorized
- [x] testFuzz_ExtractNative
- [x] testFuzz_CannotExtractNative
- [x] testFuzz_ExtractERC20
- [x] testFuzz_CannotExtractERC20
- [x] testFuzz_Receive
- [x] testFuzz_Fallback

Considerations:

Expand Down
31 changes: 31 additions & 0 deletions src/Position.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
IERC20Permit(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.
Expand Down
56 changes: 56 additions & 0 deletions src/interfaces/IPosition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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).
Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions src/interfaces/aave/IPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
*
*/
Expand Down
83 changes: 83 additions & 0 deletions src/interfaces/token/IERC20Permit.sol
Original file line number Diff line number Diff line change
@@ -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);
}
47 changes: 47 additions & 0 deletions src/services/DebtService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 { IERC20Permit } from "src/interfaces/token/IERC20Permit.sol";
import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol";
import { IERC20Metadata } from "src/interfaces/token/IERC20Metadata.sol";

Expand Down Expand Up @@ -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
IERC20Permit(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).
Expand All @@ -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
IERC20Permit(D_TOKEN).permit(msg.sender, address(this), _dAmt, _deadline, _v, _r, _s);

// 2. Repay
repayAfterClose(_dAmt, _withdrawBuffer);
}
}
3 changes: 0 additions & 3 deletions test/FeeCollector.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -500,20 +500,17 @@ contract FeeCollectorTest is Test, TokenUtils {
vm.deal(_sender, _amount + gasMoney);

// Pre-Act Data
uint256 preSenderBalance = _sender.balance;
uint256 preContractBalance = feeCollectorAddr.balance;

// Act
vm.prank(_sender);
(bool success,) = feeCollectorAddr.call{ value: _amount }(abi.encodeWithSignature("nonExistentFn()"));

// Post-Act Data
uint256 postSenderBalance = _sender.balance;
uint256 postContractBalance = feeCollectorAddr.balance;

// Assertions
assertTrue(success);
assertEq(postSenderBalance, preSenderBalance - _amount);
assertEq(postContractBalance, preContractBalance + _amount);
}
}
Loading
Loading