Merge pull request #64 from aave/feat/protocol-2.5/repayPermit-depositPermit

RepayWithPermit, depositWithPermit
This commit is contained in:
The-3D 2021-05-05 13:12:22 +02:00 committed by GitHub
commit 77fe998e9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 755 additions and 23 deletions

View File

@ -244,6 +244,57 @@ 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,
uint256 deadline,
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,
uint256 deadline,
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

View File

@ -131,6 +131,41 @@ 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
* @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,
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

View File

@ -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;
}

View File

@ -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.

View File

@ -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,36 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage
address onBehalfOf,
uint16 referralCode
) external override whenNotPaused {
DataTypes.ReserveData storage reserve = _reserves[asset];
_executeDeposit(asset, amount, onBehalfOf, referralCode);
}
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);
/**
* @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);
}
/**
@ -205,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
@ -845,6 +887,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,

View File

@ -363,3 +363,5 @@ export const getFlashLiquidationAdapter = async (address?: tEthereumAddress) =>
.address,
await getFirstSigner()
);
export const getChainId = async () => (await DRE.ethers.provider.getNetwork()).chainId;

View File

@ -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",

View File

@ -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>rateMode,
reserveDataBefore,
userDataBefore,
txTimestamp,
timestamp
);
const expectedUserData = calcExpectedUserDataAfterRepay(
amountToRepay,
<RateMode>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,

View File

@ -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;

View File

@ -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"
}
]
}
]
}

View File

@ -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 in case random HEVM network errors happen
this.retries(4);
await executeStory(story, testEnv);
});

View File

@ -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 in case random HEVM network errors happen
this.retries(4);
await executeStory(story, testEnv);
});