diff --git a/contracts/interfaces/IAToken.sol b/contracts/interfaces/IAToken.sol index cf0ea261..2367f6d6 100644 --- a/contracts/interfaces/IAToken.sol +++ b/contracts/interfaces/IAToken.sol @@ -95,6 +95,27 @@ interface IAToken is IERC20, IScaledBalanceToken, IInitializableAToken { **/ function handleRepayment(address user, uint256 amount) external; + /** + * @dev implements the permit function as for + * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md + * @param owner The owner of the funds + * @param spender The spender + * @param value The amount + * @param deadline The deadline timestamp, type(uint256).max for max deadline + * @param v Signature param + * @param s Signature param + * @param r Signature param + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + /** * @dev Returns the address of the incentives controller contract **/ diff --git a/contracts/interfaces/IAaveIncentivesController.sol b/contracts/interfaces/IAaveIncentivesController.sol index c049bd77..efa132b2 100644 --- a/contracts/interfaces/IAaveIncentivesController.sol +++ b/contracts/interfaces/IAaveIncentivesController.sol @@ -3,9 +3,126 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; interface IAaveIncentivesController { + event RewardsAccrued(address indexed user, uint256 amount); + + event RewardsClaimed(address indexed user, address indexed to, uint256 amount); + + event RewardsClaimed( + address indexed user, + address indexed to, + address indexed claimer, + uint256 amount + ); + + event ClaimerSet(address indexed user, address indexed claimer); + + /* + * @dev Returns the configuration of the distribution for a certain asset + * @param asset The address of the reference asset of the distribution + * @return The asset index, the emission per second and the last updated timestamp + **/ + function getAssetData(address asset) + external + view + returns ( + uint256, + uint256, + uint256 + ); + + /** + * @dev Whitelists an address to claim the rewards on behalf of another address + * @param user The address of the user + * @param claimer The address of the claimer + */ + function setClaimer(address user, address claimer) external; + + /** + * @dev Returns the whitelisted claimer for a certain address (0x0 if not set) + * @param user The address of the user + * @return The claimer address + */ + function getClaimer(address user) external view returns (address); + + /** + * @dev Configure assets for a certain rewards emission + * @param assets The assets to incentivize + * @param emissionsPerSecond The emission for each asset + */ + function configureAssets(address[] calldata assets, uint256[] calldata emissionsPerSecond) + external; + + /** + * @dev Called by the corresponding asset on any update that affects the rewards distribution + * @param asset The address of the user + * @param userBalance The balance of the user of the asset in the lending pool + * @param totalSupply The total supply of the asset in the lending pool + **/ function handleAction( - address user, + address asset, uint256 userBalance, uint256 totalSupply ) external; + + /** + * @dev Returns the total of rewards of an user, already accrued + not yet accrued + * @param user The address of the user + * @return The rewards + **/ + function getRewardsBalance(address[] calldata assets, address user) + external + view + returns (uint256); + + /** + * @dev Claims reward for an user, on all the assets of the lending pool, accumulating the pending rewards + * @param amount Amount of rewards to claim + * @param to Address that will be receiving the rewards + * @return Rewards claimed + **/ + function claimRewards( + address[] calldata assets, + uint256 amount, + address to + ) external returns (uint256); + + /** + * @dev Claims reward for an user on behalf, on all the assets of the lending pool, accumulating the pending rewards. The caller must + * be whitelisted via "allowClaimOnBehalf" function by the RewardsAdmin role manager + * @param amount Amount of rewards to claim + * @param user Address to check and claim rewards + * @param to Address that will be receiving the rewards + * @return Rewards claimed + **/ + function claimRewardsOnBehalf( + address[] calldata assets, + uint256 amount, + address user, + address to + ) external returns (uint256); + + /** + * @dev returns the unclaimed rewards of the user + * @param user the address of the user + * @return the unclaimed user rewards + */ + function getUserUnclaimedRewards(address user) external view returns (uint256); + + /** + * @dev returns the unclaimed rewards of the user + * @param user the address of the user + * @param asset The asset to incentivize + * @return the user index for the asset + */ + function getUserAssetData(address user, address asset) external view returns (uint256); + + /** + * @dev for backward compatibility with previous implementation of the Incentives controller + */ + function REWARD_TOKEN() external view returns (address); + + /** + * @dev for backward compatibility with previous implementation of the Incentives controller + */ + function PRECISION() external view returns (uint8); } diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 64f726c0..ab012fb8 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -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 diff --git a/contracts/misc/UiPoolDataProvider.sol b/contracts/misc/UiPoolDataProvider.sol index 65fdc4dd..63dd7a1c 100644 --- a/contracts/misc/UiPoolDataProvider.sol +++ b/contracts/misc/UiPoolDataProvider.sol @@ -4,6 +4,7 @@ pragma experimental ABIEncoderV2; import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IAaveIncentivesController} from '../interfaces/IAaveIncentivesController.sol'; import {IUiPoolDataProvider} from './interfaces/IUiPoolDataProvider.sol'; import {ILendingPool} from '../interfaces/ILendingPool.sol'; import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; @@ -24,6 +25,13 @@ contract UiPoolDataProvider is IUiPoolDataProvider { using UserConfiguration for DataTypes.UserConfigurationMap; address public constant MOCK_USD_ADDRESS = 0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96; + IAaveIncentivesController public immutable incentivesController; + IPriceOracleGetter public immutable oracle; + + constructor(IAaveIncentivesController _incentivesController, IPriceOracleGetter _oracle) public { + incentivesController = _incentivesController; + oracle = _oracle; + } function getInterestRateStrategySlopes(DefaultReserveInterestRateStrategy interestRateStrategy) internal @@ -50,11 +58,11 @@ contract UiPoolDataProvider is IUiPoolDataProvider { returns ( AggregatedReserveData[] memory, UserReserveData[] memory, + uint256, uint256 ) { ILendingPool lendingPool = ILendingPool(provider.getLendingPool()); - IPriceOracleGetter oracle = IPriceOracleGetter(provider.getPriceOracle()); address[] memory reserves = lendingPool.getReservesList(); DataTypes.UserConfigurationMap memory userConfig = lendingPool.getUserConfiguration(user); @@ -122,7 +130,43 @@ contract UiPoolDataProvider is IUiPoolDataProvider { DefaultReserveInterestRateStrategy(reserveData.interestRateStrategyAddress) ); + // incentives + if (address(0) != address(incentivesController)) { + ( + reserveData.aEmissionPerSecond, + reserveData.aIncentivesLastUpdateTimestamp, + reserveData.aTokenIncentivesIndex + ) = incentivesController.getAssetData(reserveData.aTokenAddress); + + ( + reserveData.sEmissionPerSecond, + reserveData.sIncentivesLastUpdateTimestamp, + reserveData.sTokenIncentivesIndex + ) = incentivesController.getAssetData(reserveData.stableDebtTokenAddress); + + ( + reserveData.vEmissionPerSecond, + reserveData.vIncentivesLastUpdateTimestamp, + reserveData.vTokenIncentivesIndex + ) = incentivesController.getAssetData(reserveData.variableDebtTokenAddress); + } + if (user != address(0)) { + // incentives + if (address(0) != address(incentivesController)) { + userReservesData[i].aTokenincentivesUserIndex = incentivesController.getUserAssetData( + user, + reserveData.aTokenAddress + ); + userReservesData[i].vTokenincentivesUserIndex = incentivesController.getUserAssetData( + user, + reserveData.variableDebtTokenAddress + ); + userReservesData[i].sTokenincentivesUserIndex = incentivesController.getUserAssetData( + user, + reserveData.stableDebtTokenAddress + ); + } // user reserve data userReservesData[i].underlyingAsset = reserveData.underlyingAsset; userReservesData[i].scaledATokenBalance = IAToken(reserveData.aTokenAddress) @@ -155,6 +199,12 @@ contract UiPoolDataProvider is IUiPoolDataProvider { } } } - return (reservesData, userReservesData, oracle.getAssetPrice(MOCK_USD_ADDRESS)); + + return ( + reservesData, + userReservesData, + oracle.getAssetPrice(MOCK_USD_ADDRESS), + incentivesController.getUserUnclaimedRewards(user) + ); } } diff --git a/contracts/misc/WETHGateway.sol b/contracts/misc/WETHGateway.sol index 336e8de2..39ecf909 100644 --- a/contracts/misc/WETHGateway.sol +++ b/contracts/misc/WETHGateway.sol @@ -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 diff --git a/contracts/misc/interfaces/IUiPoolDataProvider.sol b/contracts/misc/interfaces/IUiPoolDataProvider.sol index 81a553e8..34f06d1c 100644 --- a/contracts/misc/interfaces/IUiPoolDataProvider.sol +++ b/contracts/misc/interfaces/IUiPoolDataProvider.sol @@ -3,6 +3,7 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; +import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol'; interface IUiPoolDataProvider { struct AggregatedReserveData { @@ -41,12 +42,17 @@ interface IUiPoolDataProvider { uint256 variableRateSlope2; uint256 stableRateSlope1; uint256 stableRateSlope2; + // incentives + uint256 aEmissionPerSecond; + uint256 vEmissionPerSecond; + uint256 sEmissionPerSecond; + uint256 aIncentivesLastUpdateTimestamp; + uint256 vIncentivesLastUpdateTimestamp; + uint256 sIncentivesLastUpdateTimestamp; + uint256 aTokenIncentivesIndex; + uint256 vTokenIncentivesIndex; + uint256 sTokenIncentivesIndex; } - // - // struct ReserveData { - // uint256 averageStableBorrowRate; - // uint256 totalLiquidity; - // } struct UserReserveData { address underlyingAsset; @@ -56,38 +62,19 @@ interface IUiPoolDataProvider { uint256 scaledVariableDebt; uint256 principalStableDebt; uint256 stableBorrowLastUpdateTimestamp; + // incentives + uint256 aTokenincentivesUserIndex; + uint256 vTokenincentivesUserIndex; + uint256 sTokenincentivesUserIndex; } - // - // struct ATokenSupplyData { - // string name; - // string symbol; - // uint8 decimals; - // uint256 totalSupply; - // address aTokenAddress; - // } - function getReservesData(ILendingPoolAddressesProvider provider, address user) external view returns ( AggregatedReserveData[] memory, UserReserveData[] memory, + uint256, uint256 ); - - // function getUserReservesData(ILendingPoolAddressesProvider provider, address user) - // external - // view - // returns (UserReserveData[] memory); - // - // function getAllATokenSupply(ILendingPoolAddressesProvider provider) - // external - // view - // returns (ATokenSupplyData[] memory); - // - // function getATokenSupply(address[] calldata aTokens) - // external - // view - // returns (ATokenSupplyData[] memory); } 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; } 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/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index 8e38650c..def5606c 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,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); } /** @@ -144,43 +156,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint256 amount, address to ) external override whenNotPaused returns (uint256) { - DataTypes.ReserveData storage reserve = _reserves[asset]; - - address aToken = reserve.aTokenAddress; - - uint256 userBalance = IAToken(aToken).balanceOf(msg.sender); - - uint256 amountToWithdraw = amount; - - if (amount == type(uint256).max) { - amountToWithdraw = userBalance; - } - - ValidationLogic.validateWithdraw( - asset, - amountToWithdraw, - userBalance, - _reserves, - _usersConfig[msg.sender], - _reservesList, - _reservesCount, - _addressesProvider.getPriceOracle() - ); - - reserve.updateState(); - - reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw); - - if (amountToWithdraw == userBalance) { - _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false); - emit ReserveUsedAsCollateralDisabled(asset, msg.sender); - } - - IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex); - - emit Withdraw(asset, msg.sender, to, amountToWithdraw); - - return amountToWithdraw; + return _executeWithdraw(asset, amount, to); } /** @@ -206,7 +182,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address onBehalfOf ) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; - _executeBorrow( ExecuteBorrowParams( asset, @@ -239,54 +214,37 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint256 rateMode, address onBehalfOf ) external override whenNotPaused returns (uint256) { - DataTypes.ReserveData storage reserve = _reserves[asset]; + return _executeRepay(asset, amount, rateMode, onBehalfOf); + } - (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve); - - DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); - - ValidationLogic.validateRepay( - reserve, - amount, - interestRateMode, - onBehalfOf, - stableDebt, - variableDebt - ); - - uint256 paybackAmount = - interestRateMode == DataTypes.InterestRateMode.STABLE ? stableDebt : variableDebt; - - if (amount < paybackAmount) { - paybackAmount = amount; - } - - reserve.updateState(); - - if (interestRateMode == DataTypes.InterestRateMode.STABLE) { - IStableDebtToken(reserve.stableDebtTokenAddress).burn(onBehalfOf, paybackAmount); - } else { - IVariableDebtToken(reserve.variableDebtTokenAddress).burn( - onBehalfOf, - paybackAmount, - reserve.variableBorrowIndex - ); - } - - address aToken = reserve.aTokenAddress; - reserve.updateInterestRates(asset, aToken, paybackAmount, 0); - - if (stableDebt.add(variableDebt).sub(paybackAmount) == 0) { - _usersConfig[onBehalfOf].setBorrowing(reserve.id, false); - } - - IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount); - - IAToken(aToken).handleRepayment(msg.sender, paybackAmount); - - emit Repay(asset, onBehalfOf, msg.sender, paybackAmount); - - return paybackAmount; + /** + * @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); } /** @@ -929,6 +887,133 @@ 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, + address to + ) internal returns (uint256) { + DataTypes.ReserveData storage reserve = _reserves[asset]; + + address aToken = reserve.aTokenAddress; + + uint256 userBalance = IAToken(aToken).balanceOf(msg.sender); + + uint256 amountToWithdraw = amount; + + if (amount == type(uint256).max) { + amountToWithdraw = userBalance; + } + + ValidationLogic.validateWithdraw( + asset, + amountToWithdraw, + userBalance, + _reserves, + _usersConfig[msg.sender], + _reservesList, + _reservesCount, + _addressesProvider.getPriceOracle() + ); + + reserve.updateState(); + + reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw); + + if (amountToWithdraw == userBalance) { + _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(asset, msg.sender); + } + + IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex); + + emit Withdraw(asset, msg.sender, to, amountToWithdraw); + + return amountToWithdraw; + } + + function _executeRepay( + address asset, + uint256 amount, + uint256 rateMode, + address onBehalfOf + ) internal returns (uint256) { + DataTypes.ReserveData storage reserve = _reserves[asset]; + + (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve); + + DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); + + ValidationLogic.validateRepay( + reserve, + amount, + interestRateMode, + onBehalfOf, + stableDebt, + variableDebt + ); + + uint256 paybackAmount = + interestRateMode == DataTypes.InterestRateMode.STABLE ? stableDebt : variableDebt; + + if (amount < paybackAmount) { + paybackAmount = amount; + } + + reserve.updateState(); + + if (interestRateMode == DataTypes.InterestRateMode.STABLE) { + IStableDebtToken(reserve.stableDebtTokenAddress).burn(onBehalfOf, paybackAmount); + } else { + IVariableDebtToken(reserve.variableDebtTokenAddress).burn( + onBehalfOf, + paybackAmount, + reserve.variableBorrowIndex + ); + } + + address aToken = reserve.aTokenAddress; + reserve.updateInterestRates(asset, aToken, paybackAmount, 0); + + if (stableDebt.add(variableDebt).sub(paybackAmount) == 0) { + _usersConfig[onBehalfOf].setBorrowing(reserve.id, false); + } + + IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount); + + IAToken(aToken).handleRepayment(msg.sender, paybackAmount); + + emit Repay(asset, onBehalfOf, msg.sender, paybackAmount); + + return paybackAmount; + } + function _addReserveToList(address asset) internal { uint256 reservesCount = _reservesCount; diff --git a/contracts/protocol/tokenization/AToken.sol b/contracts/protocol/tokenization/AToken.sol index 46aae19f..3c4334d6 100644 --- a/contracts/protocol/tokenization/AToken.sol +++ b/contracts/protocol/tokenization/AToken.sol @@ -341,7 +341,7 @@ contract AToken is uint8 v, bytes32 r, bytes32 s - ) external { + ) external override { require(owner != address(0), 'INVALID_OWNER'); //solium-disable-next-line require(block.timestamp <= deadline, 'INVALID_EXPIRATION'); diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 07f34a91..68024d2a 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -55,12 +55,28 @@ import { registerContractInJsonDb, linkBytecode, insertContractAddressInDb, + deployContract, } from './contracts-helpers'; import { StableAndVariableTokensHelperFactory } from '../types/StableAndVariableTokensHelperFactory'; import { MintableDelegationERC20 } from '../types/MintableDelegationERC20'; import { readArtifact as buidlerReadArtifact } from '@nomiclabs/buidler/plugins'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { LendingPoolLibraryAddresses } from '../types/LendingPoolFactory'; +import { UiPoolDataProvider } from '../types'; +import { verifyContract } from './etherscan-verification'; + +export const deployUiPoolDataProvider = async ( + [incentivesController, aaveOracle]: [tEthereumAddress, tEthereumAddress], + verify?: boolean +) => { + const id = eContractid.UiPoolDataProvider; + const args: string[] = [incentivesController, aaveOracle]; + const instance = await deployContract(id, args); + if (verify) { + await verifyContract(instance.address, args); + } + return instance; +}; const readArtifact = async (id: string) => { if (DRE.network.name === eEthereumNetwork.buidlerevm) { @@ -546,7 +562,15 @@ export const deployMockVariableDebtToken = async ( }; export const deployMockAToken = async ( - args: [tEthereumAddress, tEthereumAddress, tEthereumAddress, tEthereumAddress, string, string, string], + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + string, + string, + string + ], verify?: boolean ) => { const instance = await withSaveAndVerify( 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-lock.json b/package-lock.json index 6cd1ef03..8310558b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7709,6 +7709,15 @@ "requires": { "bn.js": "^5.0.0", "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", + "dev": true, + "optional": true + } } }, "browserify-sign": { @@ -7819,9 +7828,9 @@ }, "dependencies": { "node-gyp-build": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz", - "integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "dev": true } } @@ -14241,9 +14250,9 @@ }, "dependencies": { "node-gyp-build": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz", - "integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "dev": true } } diff --git a/package.json b/package.json index dbeb8ce1..699061c5 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", @@ -60,8 +60,11 @@ "ci:clean": "rm -rf ./artifacts ./cache ./types", "print-contracts:kovan": "npm run hardhat:kovan -- print-contracts", "print-contracts:main": "npm run hardhat:main -- print-contracts", - "print-contracts:ropsten": "npm run hardhat:ropsten -- print-contracts", - "dev:deployUIProvider": "npm run hardhat:kovan deploy-UiPoolDataProvider", + "print-contracts:ropsten": "npm run hardhat:main -- print-contracts", + "dev:deployUIProvider": "hardhat --network kovan deploy-UiPoolDataProvider --verify", + "main:deployUIProvider": "hardhat --network main deploy-UiPoolDataProvider --verify", + "matic:deployUIProvider": "hardhat --network matic deploy-UiPoolDataProvider", + "mumbai:deployUIProvider": "hardhat --network mumbai deploy-UiPoolDataProvider", "dev:deployUniswapRepayAdapter": "hardhat --network kovan deploy-UniswapRepayAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", "dev:UniswapLiquiditySwapAdapter": "hardhat --network kovan deploy-UniswapLiquiditySwapAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", "main:deployUniswapRepayAdapter": "hardhat --network main deploy-UniswapRepayAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", diff --git a/tasks/deployments/deploy-UiPoolDataProvider.ts b/tasks/deployments/deploy-UiPoolDataProvider.ts index 2d696702..6cc07449 100644 --- a/tasks/deployments/deploy-UiPoolDataProvider.ts +++ b/tasks/deployments/deploy-UiPoolDataProvider.ts @@ -1,27 +1,44 @@ import { task } from 'hardhat/config'; - -import { UiPoolDataProviderFactory } from '../../types'; -import { verifyContract } from '../../helpers/etherscan-verification'; -import { eContractid } from '../../helpers/types'; +import { eContractid, eEthereumNetwork, ePolygonNetwork } from '../../helpers/types'; +import { deployUiPoolDataProvider } from '../../helpers/contracts-deployments'; task(`deploy-${eContractid.UiPoolDataProvider}`, `Deploys the UiPoolDataProvider contract`) .addFlag('verify', 'Verify UiPoolDataProvider contract via Etherscan API.') .setAction(async ({ verify }, localBRE) => { await localBRE.run('set-DRE'); - if (!localBRE.network.config.chainId) { throw new Error('INVALID_CHAIN_ID'); } + const addressesByNetwork = { + [eEthereumNetwork.kovan]: { + incentivesController: '0x0000000000000000000000000000000000000000', + aaveOracle: '0x8fb777d67e9945e2c01936e319057f9d41d559e6', + }, + [eEthereumNetwork.main]: { + incentivesController: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5', + aaveOracle: '0xa50ba011c48153de246e5192c8f9258a2ba79ca9', + }, + [ePolygonNetwork.matic]: { + incentivesController: '0x357D51124f59836DeD84c8a1730D72B749d8BC23', + aaveOracle: '0x21451bD7b528896B4AB2b9764b521D6ed641708d', + }, + [ePolygonNetwork.mumbai]: { + incentivesController: '0xd41aE58e803Edf4304334acCE4DC4Ec34a63C644', + aaveOracle: '0xC365C653f7229894F93994CD0b30947Ab69Ff1D5', + }, + }; + console.log(`\n- UiPoolDataProvider deployment`); console.log(`\tDeploying UiPoolDataProvider implementation ...`); - const uiPoolDataProvider = await new UiPoolDataProviderFactory( - await localBRE.ethers.provider.getSigner() - ).deploy(); - await uiPoolDataProvider.deployTransaction.wait(); - console.log('uiPoolDataProvider.address', uiPoolDataProvider.address); - await verifyContract(uiPoolDataProvider.address, []); + const UiPoolDataProvider = await deployUiPoolDataProvider( + [ + addressesByNetwork[localBRE.network.name].incentivesController, + addressesByNetwork[localBRE.network.name].aaveOracle, + ], + verify + ); - console.log(`\tFinished UiPoolDataProvider proxy and implementation deployment`); + console.log(`\tFinished UiPoolDataProvider deployment: ${UiPoolDataProvider.address}`); }); 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..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 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 f9c4d78b..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 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); });