diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index c6da3c32..83e40cd9 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -4,6 +4,7 @@ pragma solidity ^0.6.8; import {ReserveConfiguration} from '../libraries/configuration/ReserveConfiguration.sol'; import {UserConfiguration} from '../libraries/configuration/UserConfiguration.sol'; import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; +import {ILendingPoolAddressesProvider} from './ILendingPoolAddressesProvider.sol'; pragma experimental ABIEncoderV2; @@ -368,6 +369,8 @@ interface ILendingPool { function getReservesList() external view returns (address[] memory); + function getAddressesProvider() external view returns (ILendingPoolAddressesProvider); + /** * @dev Set the _pause state * @param val the boolean value to set the current pause state of LendingPool diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 23bf3e94..cb7f3682 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -753,7 +753,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage /** * @dev returns the addresses provider **/ - function getAddressesProvider() external view returns (ILendingPoolAddressesProvider) { + function getAddressesProvider() external override view returns (ILendingPoolAddressesProvider) { return _addressesProvider; } diff --git a/contracts/lendingpool/LendingPoolConfigurator.sol b/contracts/lendingpool/LendingPoolConfigurator.sol index 8f9c28dd..b336ed51 100644 --- a/contracts/lendingpool/LendingPoolConfigurator.sol +++ b/contracts/lendingpool/LendingPoolConfigurator.sol @@ -188,10 +188,10 @@ contract LendingPoolConfigurator is VersionedInitializable { ILendingPool internal pool; /** - * @dev only the lending pool manager can call functions affected by this modifier + * @dev only the aave admin can call functions affected by this modifier **/ modifier onlyAaveAdmin { - require(addressesProvider.getAaveAdmin() == msg.sender, Errors.LPC_CALLER_NOT_AAVE_ADMIN); + require(addressesProvider.getAaveAdmin() == msg.sender, Errors.CALLER_NOT_AAVE_ADMIN); _; } diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index 3b37e91a..a0a65762 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -17,6 +17,10 @@ pragma solidity ^0.6.8; * - P = Pausable */ library Errors { + //common errors + string public constant CALLER_NOT_AAVE_ADMIN = '33'; // 'The caller must be the aave admin' + + //contract specific errors string public constant VL_AMOUNT_NOT_GREATER_THAN_0 = '1'; // 'Amount must be greater than 0' string public constant VL_NO_ACTIVE_RESERVE = '2'; // 'Action requires an active reserve' string public constant VL_RESERVE_FROZEN = '3'; // 'Action cannot be performed because the reserve is frozen' @@ -49,7 +53,6 @@ library Errors { string public constant AT_CANNOT_GIVE_ALLVWANCE_TO_HIMSELF = '30'; // 'User cannot give allowance to himself' string public constant AT_TRANSFER_AMOUNT_NOT_GT_0 = '31'; // 'Transferred amount needs to be greater than zero' string public constant RL_RESERVE_ALREADY_INITIALIZED = '32'; // 'Reserve has already been initialized' - string public constant LPC_CALLER_NOT_AAVE_ADMIN = '33'; // 'The caller must be the aave admin' string public constant LPC_RESERVE_LIQUIDITY_NOT_0 = '34'; // 'The liquidity of the reserve needs to be 0' string public constant LPC_INVALID_ATOKEN_POOL_ADDRESS = '35'; // 'The liquidity of the reserve needs to be 0' string public constant LPC_INVALID_STABLE_DEBT_TOKEN_POOL_ADDRESS = '36'; // 'The liquidity of the reserve needs to be 0' diff --git a/contracts/mocks/tokens/MintableDelegationERC20.sol b/contracts/mocks/tokens/MintableDelegationERC20.sol new file mode 100644 index 00000000..1b3088d5 --- /dev/null +++ b/contracts/mocks/tokens/MintableDelegationERC20.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +import {ERC20} from '../../dependencies/openzeppelin/contracts/ERC20.sol'; + +/** + * @title ERC20Mintable + * @dev ERC20 minting logic + */ +contract MintableDelegationERC20 is ERC20 { + address public delegatee; + + constructor( + string memory name, + string memory symbol, + uint8 decimals + ) public ERC20(name, symbol) { + _setupDecimals(decimals); + } + + /** + * @dev Function to mint tokensp + * @param value The amount of tokens to mint. + * @return A boolean that indicates if the operation was successful. + */ + function mint(uint256 value) public returns (bool) { + _mint(msg.sender, value); + return true; + } + + function delegate(address delegateeAddress) external { + delegatee = delegateeAddress; + } +} diff --git a/contracts/tokenization/DelegationAwareAToken.sol b/contracts/tokenization/DelegationAwareAToken.sol new file mode 100644 index 00000000..12e80292 --- /dev/null +++ b/contracts/tokenization/DelegationAwareAToken.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +import {AToken} from './AToken.sol'; +import {ILendingPool} from '../interfaces/ILendingPool.sol'; +import {Errors} from '../libraries/helpers/Errors.sol'; + +/** + * @title IDelegationToken + * @dev implements an interface for tokens that have a delegation function + **/ +interface IDelegationToken { + function delegate(address delegatee) external; +} + +/** + * @title Aave AToken with delegation capabilities + * + * @dev Implementation of the interest bearing token for the Aave protocol. This version of the aToken + * adds a function which gives the Aave protocol the ability to delegate voting power of the underlying asset. + * The underlying asset needs to be compatible with the COMP delegation interface + * @author Aave + */ +contract DelegationAwareAToken is AToken { + /** + * @dev only the aave admin can call this function + **/ + modifier onlyAaveAdmin { + require( + _msgSender() == ILendingPool(POOL).getAddressesProvider().getAaveAdmin(), + Errors.CALLER_NOT_AAVE_ADMIN + ); + _; + } + + constructor( + ILendingPool pool, + address underlyingAssetAddress, + address reserveTreasury, + string memory tokenName, + string memory tokenSymbol, + address incentivesController + ) + public + AToken( + pool, + underlyingAssetAddress, + reserveTreasury, + tokenName, + tokenSymbol, + incentivesController + ) + {} + + function initialize( + uint8 _underlyingAssetDecimals, + string calldata _tokenName, + string calldata _tokenSymbol + ) external virtual override initializer { + _setName(_tokenName); + _setSymbol(_tokenSymbol); + _setDecimals(_underlyingAssetDecimals); + } + + /** + * @dev delegates voting power of the underlying asset to a specific address + * @param delegatee the address that will receive the delegation + **/ + function delegateUnderlyingTo(address delegatee) external onlyAaveAdmin { + IDelegationToken(UNDERLYING_ASSET_ADDRESS).delegate(delegatee); + } +} diff --git a/deployed-contracts.json b/deployed-contracts.json index a2173231..c2cbd017 100644 --- a/deployed-contracts.json +++ b/deployed-contracts.json @@ -163,26 +163,25 @@ }, "ReserveLogic": { "buidlerevm": { - "address": "0x78Ee8Fb9fE5abD5e347Fc94c2fb85596d1f60e3c", + "address": "0xFAe0fd738dAbc8a0426F47437322b6d026A9FD95", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } }, "GenericLogic": { "buidlerevm": { - "address": "0x920d847fE49E54C19047ba8bc236C45A8068Bca7", + "address": "0x6082731fdAba4761277Fb31299ebC782AD3bCf24", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } }, "ValidationLogic": { "buidlerevm": { - "address": "0xA4765Ff72A9F3CfE73089bb2c3a41B838DF71574", + "address": "0x8456161947DFc1fC159A0B26c025cD2b4bba0c3e", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } }, "LendingPool": { "buidlerevm": { - "address": "0x35c1419Da7cf0Ff885B8Ef8EA9242FEF6800c99b", - "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" + "address": "0xD9273d497eDBC967F39d419461CfcF382a0A822e" } }, "LendingPoolConfigurator": { @@ -198,7 +197,7 @@ }, "ATokensAndRatesHelper": { "buidlerevm": { - "address": "0x920d847fE49E54C19047ba8bc236C45A8068Bca7", + "address": "0x06bA8d8af0dF898D0712DffFb0f862cC51AF45c2", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } }, @@ -265,5 +264,17 @@ "address": "0x920d847fE49E54C19047ba8bc236C45A8068Bca7", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } + }, + "MintableDelegationERC20": { + "buidlerevm": { + "address": "0x77B0b5636fEA30eA79BB65AeCCdb599997A849A8", + "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" + } + }, + "AToken": { + "buidlerevm": { + "address": "0x78Ee8Fb9fE5abD5e347Fc94c2fb85596d1f60e3c", + "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" + } } } diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 90561b28..6c5b9c70 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -23,6 +23,7 @@ import { ATokensAndRatesHelperFactory, ChainlinkProxyPriceProviderFactory, DefaultReserveInterestRateStrategyFactory, + DelegationAwareATokenFactory, InitializableAdminUpgradeabilityProxyFactory, LendingPoolAddressesProviderFactory, LendingPoolAddressesProviderRegistryFactory, @@ -31,6 +32,7 @@ import { LendingPoolFactory, LendingPoolLibraryAddresses, LendingRateOracleFactory, + MintableDelegationErc20Factory, MintableErc20Factory, MockAggregatorFactory, MockATokenFactory, @@ -47,6 +49,7 @@ import {withSaveAndVerify, registerContractInJsonDb, linkBytecode} from './contr import {StableAndVariableTokensHelperFactory} from '../types/StableAndVariableTokensHelperFactory'; import {MockStableDebtToken} from '../types/MockStableDebtToken'; import {MockVariableDebtToken} from '../types/MockVariableDebtToken'; +import {MintableDelegationErc20} from '../types/MintableDelegationErc20'; export const deployLendingPoolAddressesProvider = async (verify?: boolean) => withSaveAndVerify( @@ -254,6 +257,16 @@ export const deployMintableERC20 = async ( verify ); +export const deployMintableDelegationERC20 = async ( + args: [string, string, string], + verify?: boolean +): Promise => + withSaveAndVerify( + await new MintableDelegationErc20Factory(await getFirstSigner()).deploy(...args), + eContractid.MintableDelegationERC20, + args, + verify + ); export const deployDefaultReserveInterestRateStrategy = async ( args: [tEthereumAddress, string, string, string, string, string], verify: boolean @@ -313,6 +326,32 @@ export const deployGenericAToken = async ( ); }; +export const deployDelegationAwareAToken = async ( + [poolAddress, underlyingAssetAddress, name, symbol, incentivesController]: [ + tEthereumAddress, + tEthereumAddress, + string, + string, + tEthereumAddress + ], + verify: boolean +) => { + const args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + string, + string, + tEthereumAddress + ] = [poolAddress, underlyingAssetAddress, ZERO_ADDRESS, name, symbol, incentivesController]; + return withSaveAndVerify( + await new DelegationAwareATokenFactory(await getFirstSigner()).deploy(...args), + eContractid.AToken, + args, + verify + ); +}; + export const deployAllMockTokens = async (verify?: boolean) => { const tokens: {[symbol: string]: MockContract | MintableERC20} = {}; diff --git a/helpers/types.ts b/helpers/types.ts index 7c4da1f4..f10938d2 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -28,6 +28,7 @@ export enum eContractid { Example = 'Example', LendingPoolAddressesProvider = 'LendingPoolAddressesProvider', MintableERC20 = 'MintableERC20', + MintableDelegationERC20 = 'MintableDelegationERC20', LendingPoolAddressesProviderRegistry = 'LendingPoolAddressesProviderRegistry', LendingPoolParametersProvider = 'LendingPoolParametersProvider', LendingPoolConfigurator = 'LendingPoolConfigurator', @@ -47,6 +48,7 @@ export enum eContractid { WalletBalanceProvider = 'WalletBalanceProvider', AToken = 'AToken', MockAToken = 'MockAToken', + DelegationAwareAToken = 'DelegationAwareAToken', MockStableDebtToken = 'MockStableDebtToken', MockVariableDebtToken = 'MockVariableDebtToken', AaveProtocolTestHelpers = 'AaveProtocolTestHelpers', @@ -72,6 +74,10 @@ export enum eContractid { * - P = Pausable */ export enum ProtocolErrors { + //common errors + CALLER_NOT_AAVE_ADMIN = '33', // 'The caller must be the aave admin' + + //contract specific errors VL_AMOUNT_NOT_GREATER_THAN_0 = '1', // 'Amount must be greater than 0' VL_NO_ACTIVE_RESERVE = '2', // 'Action requires an active reserve' VL_RESERVE_FROZEN = '3', // 'Action requires an unfrozen reserve' @@ -104,7 +110,6 @@ export enum ProtocolErrors { AT_CANNOT_GIVE_ALLVWANCE_TO_HIMSELF = '30', // 'User cannot give allowance to himself' AT_TRANSFER_AMOUNT_NOT_GT_0 = '31', // 'Transferred amount needs to be greater than zero' RL_RESERVE_ALREADY_INITIALIZED = '32', // 'Reserve has already been initialized' - LPC_CALLER_NOT_AAVE_ADMIN = '33', // 'The caller must be the aave admin' LPC_RESERVE_LIQUIDITY_NOT_0 = '34', // 'The liquidity of the reserve needs to be 0' LPC_INVALID_ATOKEN_POOL_ADDRESS = '35', // 'The liquidity of the reserve needs to be 0' LPC_INVALID_STABLE_DEBT_TOKEN_POOL_ADDRESS = '36', // 'The liquidity of the reserve needs to be 0' diff --git a/test/delegation-aware-atoken.spec.ts b/test/delegation-aware-atoken.spec.ts new file mode 100644 index 00000000..7170cc22 --- /dev/null +++ b/test/delegation-aware-atoken.spec.ts @@ -0,0 +1,58 @@ +import {MAX_UINT_AMOUNT, ZERO_ADDRESS} from '../helpers/constants'; +import {BUIDLEREVM_CHAINID} from '../helpers/buidler-constants'; +import {buildPermitParams, getSignatureFromTypedData} from '../helpers/contracts-helpers'; +import {expect} from 'chai'; +import {ethers} from 'ethers'; +import {eEthereumNetwork, ProtocolErrors} from '../helpers/types'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {BRE} from '../helpers/misc-utils'; +import { + ConfigNames, + getATokenDomainSeparatorPerNetwork, + loadPoolConfig, +} from '../helpers/configuration'; +import {waitForTx} from '../helpers/misc-utils'; +import { + deployDelegationAwareAToken, + deployMintableDelegationERC20, +} from '../helpers/contracts-deployments'; +import {DelegationAwareATokenFactory} from '../types'; +import {DelegationAwareAToken} from '../types/DelegationAwareAToken'; +import {MintableDelegationErc20} from '../types/MintableDelegationErc20'; + +const {parseEther} = ethers.utils; + +makeSuite('AToken: underlying delegation', (testEnv: TestEnv) => { + const poolConfig = loadPoolConfig(ConfigNames.Commons); + let delegationAToken = {}; + let delegationERC20 = {}; + + it('Deploys a new MintableDelegationERC20 and a DelegationAwareAToken', async () => { + const {pool} = testEnv; + + delegationERC20 = await deployMintableDelegationERC20(['DEL', 'DEL', '18']); + + delegationAToken = await deployDelegationAwareAToken( + [pool.address, delegationERC20.address, 'aDEL', 'aDEL', ZERO_ADDRESS], + false + ); + }); + + it('Tries to delegate with the caller not being the Aave admin', async () => { + const {users} = testEnv; + + await expect( + delegationAToken.connect(users[1].signer).delegateUnderlyingTo(users[2].address) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_AAVE_ADMIN); + }); + + it('Tries to delegate to user 2', async () => { + const {users} = testEnv; + + await delegationAToken.delegateUnderlyingTo(users[2].address); + + const delegateeAddress = await delegationERC20.delegatee(); + + expect(delegateeAddress).to.be.equal(users[2].address); + }); +});