From 566f9b1a5d8b0f8f76230d4aec07189c55a6898c Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Thu, 1 Apr 2021 14:23:31 +0200 Subject: [PATCH 1/9] Updated ILendingPool Interface --- contracts/interfaces/ILendingPool.sol | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 64f726c0..efcdebf4 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -244,6 +244,55 @@ interface ILendingPool { address onBehalfOf ) external returns (uint256); + /** + * @notice Deposit with transfer approval of asset to be deposited done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the underlying asset to deposit + * @param amount The amount to be deposited + * @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 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 V parameter of ERC712 permit sig + * @param permitR R parameter of ERC712 permit sig + * @param permitS S parameter of ERC712 permit sig + **/ + function depositWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external; + + /** + * @notice Repay with transfer approval of asset to be repaid done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable + * @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 permitV V parameter of ERC712 permit sig + * @param permitR R parameter of ERC712 permit sig + * @param permitS S parameter of ERC712 permit sig + * @return The final amount repaid + **/ + function repayWithPermit( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external returns (uint256); + /** * @dev Allows a borrower to swap his debt between stable and variable mode, or viceversa * @param asset The address of the underlying asset borrowed From 6810940c9f07025559825ab95e69e0cabef29380 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Fri, 2 Apr 2021 09:49:13 +0200 Subject: [PATCH 2/9] Added deadline, implemented deposit and repay withPermit functions --- contracts/interfaces/ILendingPool.sol | 2 + .../protocol/lendingpool/LendingPool.sol | 105 ++++++++++++++---- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index efcdebf4..ab012fb8 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -263,6 +263,7 @@ interface ILendingPool { uint256 amount, address onBehalfOf, uint16 referralCode, + uint256 deadline, uint8 permitV, bytes32 permitR, bytes32 permitS @@ -288,6 +289,7 @@ interface ILendingPool { uint256 amount, uint256 rateMode, address onBehalfOf, + uint256 deadline, uint8 permitV, bytes32 permitR, bytes32 permitS diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index 531f007b..a86dc4aa 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -4,6 +4,7 @@ pragma experimental ABIEncoderV2; import {SafeMath} from '../../dependencies/openzeppelin/contracts/SafeMath.sol'; import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC20WithPermit} from '../../interfaces/IERC20WithPermit.sol'; import {SafeERC20} from '../../dependencies/openzeppelin/contracts/SafeERC20.sol'; import {Address} from '../../dependencies/openzeppelin/contracts/Address.sol'; import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; @@ -107,25 +108,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address onBehalfOf, uint16 referralCode ) external override whenNotPaused { - DataTypes.ReserveData storage reserve = _reserves[asset]; - - ValidationLogic.validateDeposit(reserve, amount); - - address aToken = reserve.aTokenAddress; - - reserve.updateState(); - reserve.updateInterestRates(asset, aToken, amount, 0); - - IERC20(asset).safeTransferFrom(msg.sender, aToken, amount); - - bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex); - - if (isFirstDeposit) { - _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true); - emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); - } - - emit Deposit(asset, msg.sender, onBehalfOf, amount, referralCode); + _executeDeposit(asset, amount, onBehalfOf, referralCode); } /** @@ -254,6 +237,63 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage emit Swap(asset, msg.sender, rateMode); } + /** + * @notice Deposit with transfer approval of asset to be deposited done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the underlying asset to deposit + * @param amount The amount to be deposited + * @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 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 V parameter of ERC712 permit sig + * @param permitR R parameter of ERC712 permit sig + * @param permitS S parameter of ERC712 permit sig + **/ + function depositWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override { + IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + _executeDeposit(asset, amount, onBehalfOf, referralCode); + } + + /** + * @notice Repay with transfer approval of asset to be repaid done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable + * @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 permitV V parameter of ERC712 permit sig + * @param permitR R parameter of ERC712 permit sig + * @param permitS S parameter of ERC712 permit sig + * @return The final amount repaid + **/ + function repayWithPermit( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override returns (uint256) { + IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + return _executeRepay(asset, amount, rateMode, onBehalfOf); + } + /** * @dev Rebalances the stable interest rate of a user to the current stable rate defined on the reserve. * - Users can be rebalanced if the following conditions are satisfied: @@ -845,6 +885,33 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ); } + function _executeDeposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) internal { + DataTypes.ReserveData storage reserve = _reserves[asset]; + + ValidationLogic.validateDeposit(reserve, amount); + + address aToken = reserve.aTokenAddress; + + reserve.updateState(); + reserve.updateInterestRates(asset, aToken, amount, 0); + + IERC20(asset).safeTransferFrom(msg.sender, aToken, amount); + + bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex); + + if (isFirstDeposit) { + _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true); + emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); + } + + emit Deposit(asset, msg.sender, onBehalfOf, amount, referralCode); + } + function _executeWithdraw( address asset, uint256 amount, From 3a6948ce2c1edd6e395681f57f4f58f159e78294 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 6 Apr 2021 14:55:28 +0200 Subject: [PATCH 3/9] Added depositWithPermit, repayWithPermit test scenarios --- contracts/mocks/tokens/MintableERC20.sol | 53 ++++ helpers/contracts-getters.ts | 2 + package.json | 2 +- test-suites/test-aave/helpers/actions.ts | 258 +++++++++++++++++- .../test-aave/helpers/scenario-engine.ts | 67 +++++ .../borrow-repayWithPermit-variable.json | 191 +++++++++++++ test-suites/test-aave/scenario.spec.ts | 2 +- 7 files changed, 571 insertions(+), 4 deletions(-) create mode 100644 test-suites/test-aave/helpers/scenarios/borrow-repayWithPermit-variable.json diff --git a/contracts/mocks/tokens/MintableERC20.sol b/contracts/mocks/tokens/MintableERC20.sol index 56da583e..82e40b99 100644 --- a/contracts/mocks/tokens/MintableERC20.sol +++ b/contracts/mocks/tokens/MintableERC20.sol @@ -8,14 +8,67 @@ import {ERC20} from '../../dependencies/openzeppelin/contracts/ERC20.sol'; * @dev ERC20 minting logic */ contract MintableERC20 is ERC20 { + + bytes public constant EIP712_REVISION = bytes('1'); + bytes32 internal constant EIP712_DOMAIN = + keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'); + bytes32 public constant PERMIT_TYPEHASH = + keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); + + mapping(address => uint256) public _nonces; + + bytes32 public DOMAIN_SEPARATOR; + constructor( string memory name, string memory symbol, uint8 decimals ) public ERC20(name, symbol) { + + uint256 chainId; + + assembly { + chainId := chainid() + } + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + EIP712_DOMAIN, + keccak256(bytes(name)), + keccak256(EIP712_REVISION), + chainId, + address(this) + ) + ); _setupDecimals(decimals); } + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + require(owner != address(0), 'INVALID_OWNER'); + //solium-disable-next-line + require(block.timestamp <= deadline, 'INVALID_EXPIRATION'); + uint256 currentValidNonce = _nonces[owner]; + bytes32 digest = + keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline)) + ) + ); + require(owner == ecrecover(digest, v, r, s), 'INVALID_SIGNATURE'); + _nonces[owner] = currentValidNonce.add(1); + _approve(owner, spender, value); + } + /** * @dev Function to mint tokens * @param value The amount of tokens to mint. diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 0f70c48b..33da4054 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -363,3 +363,5 @@ export const getFlashLiquidationAdapter = async (address?: tEthereumAddress) => .address, await getFirstSigner() ); + +export const getChainId = async () => (await DRE.ethers.provider.getNetwork()).chainId; diff --git a/package.json b/package.json index 4532f6fa..defa7412 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test": "TS_NODE_TRANSPILE_ONLY=1 hardhat test ./test-suites/test-aave/*.spec.ts", "test-amm": "TS_NODE_TRANSPILE_ONLY=1 hardhat test ./test-suites/test-amm/*.spec.ts", "test-amm-scenarios": "TS_NODE_TRANSPILE_ONLY=1 hardhat test ./test-suites/test-amm/__setup.spec.ts test-suites/test-amm/scenario.spec.ts", - "test-scenarios": "npx hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/scenario.spec.ts", + "test-scenarios": "hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/scenario.spec.ts", "test-repay-with-collateral": "hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/repay-with-collateral.spec.ts", "test-liquidate-with-collateral": "hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/flash-liquidation-with-collateral.spec.ts", "test-liquidate-underlying": "hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/liquidation-underlying.spec.ts", diff --git a/test-suites/test-aave/helpers/actions.ts b/test-suites/test-aave/helpers/actions.ts index 3000576a..fac619f4 100644 --- a/test-suites/test-aave/helpers/actions.ts +++ b/test-suites/test-aave/helpers/actions.ts @@ -16,6 +16,7 @@ import { calcExpectedUserDataAfterWithdraw, } from './utils/calculations'; import { getReserveAddressFromSymbol, getReserveData, getUserData } from './utils/helpers'; +import { buildPermitParams, getSignatureFromTypedData } from '../../../helpers/contracts-helpers'; import { convertToCurrencyDecimals } from '../../../helpers/contracts-helpers'; import { @@ -23,6 +24,7 @@ import { getMintableERC20, getStableDebtToken, getVariableDebtToken, + getChainId, } from '../../../helpers/contracts-getters'; import { MAX_UINT_AMOUNT, ONE_YEAR } from '../../../helpers/constants'; import { SignerWithAddress, TestEnv } from './make-suite'; @@ -30,9 +32,10 @@ import { advanceTimeAndBlock, DRE, timeLatest, waitForTx } from '../../../helper import chai from 'chai'; import { ReserveData, UserReserveData } from './utils/interfaces'; -import { ContractReceipt } from 'ethers'; +import { ContractReceipt, Wallet } from 'ethers'; import { AToken } from '../../../types/AToken'; import { RateMode, tEthereumAddress } from '../../../helpers/types'; +import { MintableERC20Factory } from '../../../types'; const { expect } = chai; @@ -349,7 +352,7 @@ export const borrow = async ( ); const amountToBorrow = await convertToCurrencyDecimals(reserve, amount); - + if (expectedResult === 'success') { const txResult = await waitForTx( await pool @@ -515,6 +518,257 @@ export const repay = async ( } }; +export const depositWithPermit = async ( + reserveSymbol: string, + amount: string, + sender: SignerWithAddress, + senderPk: string, + onBehalfOf: tEthereumAddress, + sendValue: string, + expectedResult: string, + testEnv: TestEnv, + revertMessage?: string +) => { + const { pool } = testEnv; + + const reserve = await getReserveAddressFromSymbol(reserveSymbol); + const amountToDeposit = await convertToCurrencyDecimals(reserve, amount); + + const chainId = await getChainId(); + const token = new MintableERC20Factory(sender.signer).attach(reserve); + const highDeadline = '100000000000000000000000000'; + const nonce = await token._nonces(sender.address); + + const msgParams = buildPermitParams( + chainId, + reserve, + '1', + reserveSymbol, + sender.address, + pool.address, + nonce.toNumber(), + highDeadline, + amountToDeposit.toString() + ); + const { v, r, s } = getSignatureFromTypedData(senderPk, msgParams); + + const txOptions: any = {}; + + const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData( + reserve, + onBehalfOf, + testEnv, + sender.address + ); + + if (sendValue) { + txOptions.value = await convertToCurrencyDecimals(reserve, sendValue); + } + + if (expectedResult === 'success') { + const txResult = await waitForTx( + await pool + .connect(sender.signer) + .depositWithPermit( + reserve, + amountToDeposit, + onBehalfOf, + '0', + highDeadline, + v, + r, + s, + txOptions + ) + ); + + const { + reserveData: reserveDataAfter, + userData: userDataAfter, + timestamp, + } = await getContractsData(reserve, onBehalfOf, testEnv, sender.address); + + const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult); + + const expectedReserveData = calcExpectedReserveDataAfterDeposit( + amountToDeposit.toString(), + reserveDataBefore, + txTimestamp + ); + + const expectedUserReserveData = calcExpectedUserDataAfterDeposit( + amountToDeposit.toString(), + reserveDataBefore, + expectedReserveData, + userDataBefore, + txTimestamp, + timestamp, + txCost + ); + + expectEqual(reserveDataAfter, expectedReserveData); + expectEqual(userDataAfter, expectedUserReserveData); + + // truffleAssert.eventEmitted(txResult, "Deposit", (ev: any) => { + // const {_reserve, _user, _amount} = ev; + // return ( + // _reserve === reserve && + // _user === user && + // new BigNumber(_amount).isEqualTo(new BigNumber(amountToDeposit)) + // ); + // }); + } else if (expectedResult === 'revert') { + await expect( + pool + .connect(sender.signer) + .depositWithPermit( + reserve, + amountToDeposit, + onBehalfOf, + '0', + highDeadline, + v, + r, + s, + txOptions + ), + revertMessage + ).to.be.reverted; + } +}; + +export const repayWithPermit = async ( + reserveSymbol: string, + amount: string, + rateMode: string, + user: SignerWithAddress, + userPk: string, + onBehalfOf: SignerWithAddress, + sendValue: string, + expectedResult: string, + testEnv: TestEnv, + revertMessage?: string +) => { + const { pool } = testEnv; + const reserve = await getReserveAddressFromSymbol(reserveSymbol); + const highDeadline = '100000000000000000000000000'; + + const { reserveData: reserveDataBefore, userData: userDataBefore } = await getContractsData( + reserve, + onBehalfOf.address, + testEnv + ); + + let amountToRepay = '0'; + + if (amount !== '-1') { + amountToRepay = (await convertToCurrencyDecimals(reserve, amount)).toString(); + } else { + amountToRepay = MAX_UINT_AMOUNT; + } + amountToRepay = '0x' + new BigNumber(amountToRepay).toString(16); + + const chainId = await getChainId(); + const token = new MintableERC20Factory(user.signer).attach(reserve); + const nonce = await token._nonces(user.address); + + const msgParams = buildPermitParams( + chainId, + reserve, + '1', + reserveSymbol, + user.address, + pool.address, + nonce.toNumber(), + highDeadline, + amountToRepay + ); + const { v, r, s } = getSignatureFromTypedData(userPk, msgParams); + const txOptions: any = {}; + + if (sendValue) { + const valueToSend = await convertToCurrencyDecimals(reserve, sendValue); + txOptions.value = '0x' + new BigNumber(valueToSend.toString()).toString(16); + } + + if (expectedResult === 'success') { + const txResult = await waitForTx( + await pool + .connect(user.signer) + .repayWithPermit( + reserve, + amountToRepay, + rateMode, + onBehalfOf.address, + highDeadline, + v, + r, + s, + txOptions + ) + ); + + const { txCost, txTimestamp } = await getTxCostAndTimestamp(txResult); + + const { + reserveData: reserveDataAfter, + userData: userDataAfter, + timestamp, + } = await getContractsData(reserve, onBehalfOf.address, testEnv); + + const expectedReserveData = calcExpectedReserveDataAfterRepay( + amountToRepay, + rateMode, + reserveDataBefore, + userDataBefore, + txTimestamp, + timestamp + ); + + const expectedUserData = calcExpectedUserDataAfterRepay( + amountToRepay, + rateMode, + reserveDataBefore, + expectedReserveData, + userDataBefore, + user.address, + onBehalfOf.address, + txTimestamp, + timestamp + ); + + expectEqual(reserveDataAfter, expectedReserveData); + expectEqual(userDataAfter, expectedUserData); + + // truffleAssert.eventEmitted(txResult, "Repay", (ev: any) => { + // const {_reserve, _user, _repayer} = ev; + + // return ( + // _reserve.toLowerCase() === reserve.toLowerCase() && + // _user.toLowerCase() === onBehalfOf.toLowerCase() && + // _repayer.toLowerCase() === user.toLowerCase() + // ); + // }); + } else if (expectedResult === 'revert') { + await expect( + pool + .connect(user.signer) + .repayWithPermit( + reserve, + amountToRepay, + rateMode, + onBehalfOf.address, + highDeadline, + v, + r, + s, + txOptions + ), + revertMessage + ).to.be.reverted; + } +}; + export const setUseAsCollateral = async ( reserveSymbol: string, user: SignerWithAddress, diff --git a/test-suites/test-aave/helpers/scenario-engine.ts b/test-suites/test-aave/helpers/scenario-engine.ts index 492fa8b2..7ec22bd5 100644 --- a/test-suites/test-aave/helpers/scenario-engine.ts +++ b/test-suites/test-aave/helpers/scenario-engine.ts @@ -10,8 +10,11 @@ import { swapBorrowRateMode, rebalanceStableBorrowRate, delegateBorrowAllowance, + repayWithPermit, + depositWithPermit, } from './actions'; import { RateMode } from '../../../helpers/types'; +import { Wallet } from '@ethersproject/wallet'; export interface Action { name: string; @@ -73,6 +76,12 @@ const executeAction = async (action: Action, users: SignerWithAddress[], testEnv const user = users[parseInt(userIndex)]; + const userPrivateKey = require('../../../test-wallets.js').accounts[parseInt(userIndex) + 1] + .secretKey; + if (!userPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + switch (name) { case 'mint': const { amount } = action.args; @@ -111,6 +120,30 @@ const executeAction = async (action: Action, users: SignerWithAddress[], testEnv ); } break; + case 'depositWithPermit': + { + const { amount, sendValue, onBehalfOf: onBehalfOfIndex } = action.args; + const onBehalfOf = onBehalfOfIndex + ? users[parseInt(onBehalfOfIndex)].address + : user.address; + + if (!amount || amount === '') { + throw `Invalid amount to deposit into the ${reserve} reserve`; + } + + await depositWithPermit( + reserve, + amount, + user, + userPrivateKey, + onBehalfOf, + sendValue, + expected, + testEnv, + revertMessage + ); + } + break; case 'delegateBorrowAllowance': { @@ -203,6 +236,40 @@ const executeAction = async (action: Action, users: SignerWithAddress[], testEnv } break; + case 'repayWithPermit': + { + const { amount, borrowRateMode, sendValue, deadline } = action.args; + let { onBehalfOf: onBehalfOfIndex } = action.args; + + if (!amount || amount === '') { + throw `Invalid amount to repay into the ${reserve} reserve`; + } + + let userToRepayOnBehalf: SignerWithAddress; + if (!onBehalfOfIndex || onBehalfOfIndex === '') { + console.log( + 'WARNING: No onBehalfOf specified for a repay action. Defaulting to the repayer address' + ); + userToRepayOnBehalf = user; + } else { + userToRepayOnBehalf = users[parseInt(onBehalfOfIndex)]; + } + + await repayWithPermit( + reserve, + amount, + rateMode, + user, + userPrivateKey, + userToRepayOnBehalf, + sendValue, + expected, + testEnv, + revertMessage + ); + } + break; + case 'setUseAsCollateral': { const { useAsCollateral } = action.args; diff --git a/test-suites/test-aave/helpers/scenarios/borrow-repayWithPermit-variable.json b/test-suites/test-aave/helpers/scenarios/borrow-repayWithPermit-variable.json new file mode 100644 index 00000000..85f5c723 --- /dev/null +++ b/test-suites/test-aave/helpers/scenarios/borrow-repayWithPermit-variable.json @@ -0,0 +1,191 @@ +{ + "title": "LendingPool: Borrow/repay with permit with Permit (variable rate)", + "description": "Test cases for the borrow function, variable mode.", + "stories": [ + { + "description": "User 2 deposits with permit 1 DAI to account for rounding errors", + "actions": [ + { + "name": "mint", + "args": { + "reserve": "DAI", + "amount": "1", + "user": "2" + }, + "expected": "success" + }, + { + "name": "depositWithPermit", + "args": { + "reserve": "DAI", + "amount": "1", + "user": "2" + }, + "expected": "success" + } + ] + }, + { + "description": "User 0 deposits with permit 1000 DAI, user 1 deposits 1 WETH as collateral and borrows 100 DAI at variable rate", + "actions": [ + { + "name": "mint", + "args": { + "reserve": "DAI", + "amount": "1000", + "user": "0" + }, + "expected": "success" + }, + { + "name": "depositWithPermit", + "args": { + "reserve": "DAI", + "amount": "1000", + "user": "0" + }, + "expected": "success" + }, + { + "name": "mint", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "1" + }, + "expected": "success" + }, + { + "name": "approve", + "args": { + "reserve": "WETH", + "user": "1" + }, + "expected": "success" + }, + { + "name": "deposit", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "1" + }, + "expected": "success" + }, + { + "name": "borrow", + "args": { + "reserve": "DAI", + "amount": "100", + "borrowRateMode": "variable", + "user": "1", + "timeTravel": "365" + }, + "expected": "success" + } + ] + }, + { + "description": "User 1 tries to borrow the rest of the DAI liquidity (revert expected)", + "actions": [ + { + "name": "borrow", + "args": { + "reserve": "DAI", + "amount": "900", + "borrowRateMode": "variable", + "user": "1" + }, + "expected": "revert", + "revertMessage": "There is not enough collateral to cover a new borrow" + } + ] + }, + { + "description": "User 1 tries to repay with permit 0 DAI (revert expected)", + "actions": [ + { + "name": "repayWithPermit", + "args": { + "reserve": "DAI", + "amount": "0", + "user": "1", + "onBehalfOf": "1" + }, + "expected": "revert", + "revertMessage": "Amount must be greater than 0" + } + ] + }, + { + "description": "User 1 repays with permit a small amount of DAI, enough to cover a small part of the interest", + "actions": [ + { + "name": "repayWithPermit", + "args": { + "reserve": "DAI", + "amount": "1.25", + "user": "1", + "onBehalfOf": "1", + "borrowRateMode": "variable" + }, + "expected": "success" + } + ] + }, + { + "description": "User 1 repays with permit the DAI borrow after one year", + "actions": [ + { + "name": "mint", + "description": "Mint 10 DAI to cover the interest", + "args": { + "reserve": "DAI", + "amount": "10", + "user": "1" + }, + "expected": "success" + }, + { + "name": "repayWithPermit", + "args": { + "reserve": "DAI", + "amount": "-1", + "user": "1", + "onBehalfOf": "1", + "borrowRateMode": "variable" + }, + "expected": "success" + } + ] + }, + { + "description": "User 0 withdraws the deposited DAI plus interest", + "actions": [ + { + "name": "withdraw", + "args": { + "reserve": "DAI", + "amount": "-1", + "user": "0" + }, + "expected": "success" + } + ] + }, + { + "description": "User 1 withdraws the collateral", + "actions": [ + { + "name": "withdraw", + "args": { + "reserve": "WETH", + "amount": "-1", + "user": "1" + }, + "expected": "success" + } + ] + } + ] +} diff --git a/test-suites/test-aave/scenario.spec.ts b/test-suites/test-aave/scenario.spec.ts index 68b792d5..e658948a 100644 --- a/test-suites/test-aave/scenario.spec.ts +++ b/test-suites/test-aave/scenario.spec.ts @@ -35,7 +35,7 @@ fs.readdirSync(scenarioFolder).forEach((file) => { for (const story of scenario.stories) { it(story.description, async function () { - // Retry the test scenarios up to 4 times if an error happens, due erratic HEVM network errors + // Retry the test scenarios up to 4 times if an error happens, due erratic HEVM network errors this.retries(4); await executeStory(story, testEnv); }); From 1316cf48e4b605cda20caae6846564906bf0664c Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 6 Apr 2021 15:08:19 +0200 Subject: [PATCH 4/9] Updated WETHGateway for withdrawETHWithPermit --- contracts/misc/WETHGateway.sol | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/contracts/misc/WETHGateway.sol b/contracts/misc/WETHGateway.sol index 336e8de2..1ddceaa0 100644 --- a/contracts/misc/WETHGateway.sol +++ b/contracts/misc/WETHGateway.sol @@ -131,6 +131,37 @@ contract WETHGateway is IWETHGateway, Ownable { _safeTransferETH(msg.sender, amount); } + /** + * @dev withdraws the WETH _reserves of msg.sender. + * @param lendingPool address of the targeted underlying lending pool + * @param amount amount of aWETH to withdraw and receive native ETH + * @param to address of the user who will receive native ETH + */ + function withdrawETHWithPermit( + address lendingPool, + uint256 amount, + address to, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override { + IAToken aWETH = IAToken(ILendingPool(lendingPool).getReserveData(address(WETH)).aTokenAddress); + uint256 userBalance = aWETH.balanceOf(msg.sender); + uint256 amountToWithdraw = amount; + + // if amount is equal to uint(-1), the user wants to redeem everything + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + // chosing to permit `amount`and not `amountToWithdraw`, easier for frontends, intregrators. + aWETH.permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + aWETH.transferFrom(msg.sender, address(this), amountToWithdraw); + ILendingPool(lendingPool).withdraw(address(WETH), amountToWithdraw, address(this)); + WETH.withdraw(amountToWithdraw); + _safeTransferETH(to, amountToWithdraw); + } + /** * @dev transfer ETH to an address, revert if it fails. * @param to recipient of the transfer From 5facb2a14d1c9413893ae7a7a2ec6466f587e5b0 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 6 Apr 2021 15:18:31 +0200 Subject: [PATCH 5/9] Added new IWETHGateway interface --- contracts/misc/interfaces/IWETHGateway.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/misc/interfaces/IWETHGateway.sol b/contracts/misc/interfaces/IWETHGateway.sol index 78d913cd..326ece57 100644 --- a/contracts/misc/interfaces/IWETHGateway.sol +++ b/contracts/misc/interfaces/IWETHGateway.sol @@ -27,4 +27,14 @@ interface IWETHGateway { uint256 interesRateMode, uint16 referralCode ) external; + + function withdrawETHWithPermit( + address lendingPool, + uint256 amount, + address to, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external; } From 0b8952887168fb40629fcec3a7d59f087706b3d4 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 27 Apr 2021 09:44:18 +0200 Subject: [PATCH 6/9] doc: added signature paramters to natspec docs --- contracts/misc/WETHGateway.sol | 4 ++++ contracts/protocol/lendingpool/LendingPool.sol | 2 ++ 2 files changed, 6 insertions(+) diff --git a/contracts/misc/WETHGateway.sol b/contracts/misc/WETHGateway.sol index 1ddceaa0..39ecf909 100644 --- a/contracts/misc/WETHGateway.sol +++ b/contracts/misc/WETHGateway.sol @@ -136,6 +136,10 @@ contract WETHGateway is IWETHGateway, Ownable { * @param lendingPool address of the targeted underlying lending pool * @param amount amount of aWETH to withdraw and receive native ETH * @param to address of the user who will receive native ETH + * @param deadline validity deadline of permit and so depositWithPermit signature + * @param permitV V parameter of ERC712 permit sig + * @param permitR R parameter of ERC712 permit sig + * @param permitS S parameter of ERC712 permit sig */ function withdrawETHWithPermit( address lendingPool, diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index a86dc4aa..664b2fe3 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -247,6 +247,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * is a different wallet * @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 deadline validity deadline of permit and so depositWithPermit signature * @param permitV V parameter of ERC712 permit sig * @param permitR R parameter of ERC712 permit sig * @param permitS S parameter of ERC712 permit sig @@ -275,6 +276,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * @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 validity deadline of permit and so depositWithPermit signature * @param permitV V parameter of ERC712 permit sig * @param permitR R parameter of ERC712 permit sig * @param permitS S parameter of ERC712 permit sig From b294a96ef45d9c133042b471c36341157a28f97f Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 27 Apr 2021 09:45:49 +0200 Subject: [PATCH 7/9] refactor: changed function definitions order for more clarity --- .../protocol/lendingpool/LendingPool.sol | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index 664b2fe3..def5606c 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -111,6 +111,35 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _executeDeposit(asset, amount, onBehalfOf, referralCode); } + /** + * @notice Deposit with transfer approval of asset to be deposited done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the underlying asset to deposit + * @param amount The amount to be deposited + * @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 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 deadline validity deadline of permit and so depositWithPermit signature + * @param permitV V parameter of ERC712 permit sig + * @param permitR R parameter of ERC712 permit sig + * @param permitS S parameter of ERC712 permit sig + **/ + function depositWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override { + IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + _executeDeposit(asset, amount, onBehalfOf, referralCode); + } + /** * @dev Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC @@ -188,6 +217,36 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage return _executeRepay(asset, amount, rateMode, onBehalfOf); } + /** + * @notice Repay with transfer approval of asset to be repaid done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable + * @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 validity deadline of permit and so depositWithPermit signature + * @param permitV V parameter of ERC712 permit sig + * @param permitR R parameter of ERC712 permit sig + * @param permitS S parameter of ERC712 permit sig + * @return The final amount repaid + **/ + function repayWithPermit( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external override returns (uint256) { + IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + return _executeRepay(asset, amount, rateMode, onBehalfOf); + } + /** * @dev Allows a borrower to swap his debt between stable and variable mode, or viceversa * @param asset The address of the underlying asset borrowed @@ -237,65 +296,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage emit Swap(asset, msg.sender, rateMode); } - /** - * @notice Deposit with transfer approval of asset to be deposited done via permit function - * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 - * @param asset The address of the underlying asset to deposit - * @param amount The amount to be deposited - * @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 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 deadline validity deadline of permit and so depositWithPermit signature - * @param permitV V parameter of ERC712 permit sig - * @param permitR R parameter of ERC712 permit sig - * @param permitS S parameter of ERC712 permit sig - **/ - function depositWithPermit( - address asset, - uint256 amount, - address onBehalfOf, - uint16 referralCode, - uint256 deadline, - uint8 permitV, - bytes32 permitR, - bytes32 permitS - ) external override { - IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); - _executeDeposit(asset, amount, onBehalfOf, referralCode); - } - - /** - * @notice Repay with transfer approval of asset to be repaid done via permit function - * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 - * @param asset The address of the borrowed underlying asset previously borrowed - * @param amount The amount to repay - * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` - * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable - * @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 validity deadline of permit and so depositWithPermit signature - * @param permitV V parameter of ERC712 permit sig - * @param permitR R parameter of ERC712 permit sig - * @param permitS S parameter of ERC712 permit sig - * @return The final amount repaid - **/ - function repayWithPermit( - address asset, - uint256 amount, - uint256 rateMode, - address onBehalfOf, - uint256 deadline, - uint8 permitV, - bytes32 permitR, - bytes32 permitS - ) external override returns (uint256) { - IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); - return _executeRepay(asset, amount, rateMode, onBehalfOf); - } - /** * @dev Rebalances the stable interest rate of a user to the current stable rate defined on the reserve. * - Users can be rebalanced if the following conditions are satisfied: From 10a083d8f054d0b9705f7823505fce6eae8a250a Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Tue, 27 Apr 2021 09:46:39 +0200 Subject: [PATCH 8/9] fix: typo --- test-suites/test-aave/scenario.spec.ts | 2 +- test-suites/test-amm/scenario.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-suites/test-aave/scenario.spec.ts b/test-suites/test-aave/scenario.spec.ts index e658948a..3a5ee6eb 100644 --- a/test-suites/test-aave/scenario.spec.ts +++ b/test-suites/test-aave/scenario.spec.ts @@ -35,7 +35,7 @@ fs.readdirSync(scenarioFolder).forEach((file) => { for (const story of scenario.stories) { it(story.description, async function () { - // Retry the test scenarios up to 4 times if an error happens, due erratic HEVM network errors + // Retry the test scenarios up to 4 times if an error happens, due to erratic HEVM network errors this.retries(4); await executeStory(story, testEnv); }); diff --git a/test-suites/test-amm/scenario.spec.ts b/test-suites/test-amm/scenario.spec.ts index f9c4d78b..4f663e4f 100644 --- a/test-suites/test-amm/scenario.spec.ts +++ b/test-suites/test-amm/scenario.spec.ts @@ -35,7 +35,7 @@ fs.readdirSync(scenarioFolder).forEach((file) => { for (const story of scenario.stories) { it(story.description, async function () { - // Retry the test scenarios up to 4 times if an error happens, due erratic HEVM network errors + // Retry the test scenarios up to 4 times if an error happens, due to erratic HEVM network errors this.retries(4); await executeStory(story, testEnv); }); From 39ab410ba58429eb5231d98f90076403112bca60 Mon Sep 17 00:00:00 2001 From: Hadrien Charlanes Date: Wed, 5 May 2021 13:05:02 +0200 Subject: [PATCH 9/9] fix: grammar comment --- test-suites/test-aave/scenario.spec.ts | 2 +- test-suites/test-amm/scenario.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-suites/test-aave/scenario.spec.ts b/test-suites/test-aave/scenario.spec.ts index 3a5ee6eb..8eeb0a95 100644 --- a/test-suites/test-aave/scenario.spec.ts +++ b/test-suites/test-aave/scenario.spec.ts @@ -35,7 +35,7 @@ fs.readdirSync(scenarioFolder).forEach((file) => { for (const story of scenario.stories) { it(story.description, async function () { - // Retry the test scenarios up to 4 times if an error happens, due to erratic HEVM network errors + // Retry the test scenarios up to 4 times in case random HEVM network errors happen this.retries(4); await executeStory(story, testEnv); }); diff --git a/test-suites/test-amm/scenario.spec.ts b/test-suites/test-amm/scenario.spec.ts index 4f663e4f..0e457217 100644 --- a/test-suites/test-amm/scenario.spec.ts +++ b/test-suites/test-amm/scenario.spec.ts @@ -35,7 +35,7 @@ fs.readdirSync(scenarioFolder).forEach((file) => { for (const story of scenario.stories) { it(story.description, async function () { - // Retry the test scenarios up to 4 times if an error happens, due to erratic HEVM network errors + // Retry the test scenarios up to 4 times in case random HEVM network errors happen this.retries(4); await executeStory(story, testEnv); });