diff --git a/.gitmodules b/.gitmodules index ae9d5ae..a8313b2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/aave-address-book"] path = lib/aave-address-book url = https://github.com/bgd-labs/aave-address-book +[submodule "lib/aave-governance-v3"] + path = lib/aave-governance-v3 + url = https://github.com/bgd-labs/aave-governance-v3 diff --git a/Makefile b/Makefile index 9f59d7c..cbaf2ee 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ test :; forge test -vvv test-sd-rewards :; forge test -vvv --match-contract EmissionTestSDPolygon test-stmatic-rewards :; forge test -vvv --match-contract EmissionTestSTMATICPolygon test-maticx-rewards :; forge test -vvv --match-contract EmissionTestMATICXPolygon +deploy-ledger :; forge script ${contract} --rpc-url ${chain} --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} -vvvv --slow --broadcast +deploy-private-key :; forge script ${contract} --rpc-url ${chain} --private-key ${private_key} -vvvv --slow --broadcast # scripts deploy-sd-transfer-strategy :; forge script scripts/RewardsConfigHelpers.s.sol:SDDeployTransferStrategy --rpc-url polygon --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv diff --git a/README.md b/README.md index 9489311..3e857b0 100644 --- a/README.md +++ b/README.md @@ -2,104 +2,143 @@ This repository contains: -- an [example proposal](./src/contracts/AddEmissionAdminPayload.sol) payload which could be used to setup liquidity mining on a governance controlled aave v3 pool +- an [example proposal](./src/contracts/AddEmissionAdminPayload.sol) payload which could be used to set up liquidity mining on a governance controlled aave v3 pool - a [test](./tests/EmissionTestOpOptimism.t.sol) simulating the configuration of certain assets to receive liquidity mining -- a [test](./tests/EmissionConfigurationTestMATICXPolygon.t.sol) simulating the setting up of new configuration of certain assets after the liquidity mining program has been created +- a [test](./tests/EmissionConfigurationTestMATICXPolygon.t.sol) simulating the setting up of a new configuration of certain assets after the liquidity mining program has been created -## Instructions to activate Liquidity Mining on Aave V3: +## Instructions to set up new LM program on Aave V3: -Screenshot 2023-04-10 at 11 27 10 AM + 1. Make sure the rewards funds that are needed to be distributed for Liquidity Mining are present in the Rewards Vault. - _Note: The Rewards Vault is your address which contains the reward asset._ + _Note: The Rewards Vault is your address which contains the reward asset._ -2. Do an ERC-20 approve of the total rewards to be distributed to the Transfer Strategy contract, this is contract by Aave which helps to pull the Liquidity Mining rewards from the Rewards Vault address to distribute to the user. To know more about how Transfer Strategy contract works you can check [here](https://github.com/aave/aave-v3-periphery/blob/master/docs/rewards/rewards-transfer-strategies.md). +2. Do an ERC-20 approve of the total rewards to be distributed to the Transfer Strategy contract, this is a contract by Aave that helps to pull the Liquidity Mining rewards from the Rewards Vault address to distribute to the user. To know more about how a Transfer Strategy contract works you can check [here](https://github.com/aave/aave-v3-periphery/blob/master/docs/rewards/rewards-transfer-strategies.md). - _Note: The Emission Admin is an address which has access to manage and configure the reward emissions by calling the Emission Manager contract and the general type of Transfer Strategy contract used for Liquidity Mining is of type PullRewardsStrategy._ + _Note: The general type of Transfer Strategy contract used for Liquidity Mining is of type PullRewardsStrategy._ -3. Finally we need to configure the Liquidity Mining emissions on the Emission Manager contract from the Emission Admin by calling the `configureAssets()` function which will take the array of the following struct to configure liquidity mining for mulitple assets for the same reward or multiple assets for mutiple rewards. +3. Encode Emission's Manager `configureAssets()` function call. It takes the array of the following struct to configure liquidity mining for multiple assets for the same reward or multiple assets for multiple rewards. - ``` - EMISSION_MANAGER.configureAssets([{ + _Note: Emission Manager is responsible for configuring emissions of Aave V3. You can interact with it only via the Permissioned Payloads Controller. Permissioned Payloads controller is responsible for storing and executing payloads. It is needed to have a delay between proposal creation and application_ - emissionPerSecond: The emission per second following rewards unit decimals. + ``` + abi.encodeWithSelector(EMISSION_MANAGER.configureAssets.selector, [{ - totalSupply: The total supply of the asset to incentivize. This should be kept as 0 as the Emissions Manager will fill this up. + emissionPerSecond: The emission per second following rewards unit decimals. - distributionEnd: The end of the distribution of rewards (in seconds). + totalSupply: The total supply of the asset to incentivize. This should be kept as 0 as the Emissions Manager will fill this up. - asset: The asset for which rewards should be given. Should be the address of the aave aToken (for deposit) or debtToken (for borrow). - In case where the asset for reward is for debt token please put the address of stable debt token for rewards in stable borrow mode - and address of variable debt token for rewards in variable borrow mode. + distributionEnd: The end of the distribution of rewards (in seconds). - reward: The reward token address to be used for Liquidity Mining for the asset. + asset: The asset for which rewards should be given. Should be the address of the aave aToken (for deposit) or debtToken (for borrow). + In the case where the asset for reward is for debt token please put the address of stable debt token for rewards in stable borrow mode + and address of variable debt token for rewards in variable borrow mode. - transferStrategy: The address of transfer strategy contract. + reward: The reward token address to be used for Liquidity Mining for the asset. - rewardOracle: The Chainlink Aggregator compatible Price Oracle of the reward (used on off-chain infra like UI for price conversion). + transferStrategy: The address of the transfer strategy contract. - }]) - ``` + rewardOracle: The Chainlink Aggregator compatible Price Oracle of the reward (used on off-chain infra like UI for price conversion). -Below is an example with the pseudo code to activate Liquidity Mining for the variable borrow of `wMatic` with `MaticX` as the reward token for the total amount of `60,000` `MaticX` for the total duration of `6 months`. For a more detailed explanation checkout this [test](./tests/EmissionTestMATICXPolygon.t.sol). + }]) + ``` -1. Make sure the Rewards Vault has sufficient balance of the MaticX token. +4. Finally, to create a proposal on the Permissioned Payloads Controller from the Payloads Manager you need to call the `createPayload()` function which will take the payload received in step 3. Before calling `createPayload()` you'll also need to wrap your payload in the following structure. Each field of the structure shouldn't differ from one proposal to another, except the `calldata` field. To know more about how Permissioned payloads controller works you can check [here](https://github.com/bgd-labs/aave-governance-v3/blob/main/docs/permissioned-payloads-controller-overview.md). - ``` - IERC20(MATIC_X_ADDRESS).balanceOf(REWARDS_VAULT) > 60000 *1e18 - ``` + _Note: only a user with the Payloads Manager role can create LM configuration proposals. It's needed to interact with the Permissioned Payloads Controller._ -2. Do an ERC-20 approve from the MaticX token from the Rewards Vault to the transfer strategy contract for the total amount. + ``` + PERMISSIONED_PAYLOADS_CONTROLLER.createPayload({ - ``` - IERC20(MATIC_X_ADDRESS).approve(TRANSFER_STRATEGY_ADDRESS, 60000 *1e18); - ``` + target - address of Emission Manager. -3. Configure the Liquidity Mining emissions on the Emission Manager contract. + withDelegateCall - has to always be false. Otherwise, the Payloads controller won't have permission to configure LM. - ``` - EMISSION_MANAGER.configureAssets([{ + accessLevel - has to always be PayloadsControllerUtils.AccessLevel.Level_1, reverts otherwise. - emissionPerSecond: 60000 * 1e18 / (180 days in seconds) + value - has to always be 0. Otherwise, the proposal won't be executable. - totalSupply: 0 + string signature - has to always be empty. - distributionEnd: current timestamp + (180 days in seconds) + bytes callData - payload received on step 3. + + }); + ``` - asset: Aave Variable Debt Token of wMatic // 0x4a1c3aD6Ed28a636ee1751C69071f6be75DEb8B8 +Below is an example with the pseudo-code to create a Liquidity Mining configuration proposal for the variable borrow of `wMatic` with `MaticX` as the reward token for the total amount of `60,000` `MaticX` for the total duration of `6 months`. For a more detailed explanation check out this [test](./tests/PermissionedControllerEmissionTestMATICXPolygon.t.sol). - reward: MaticX Token address // 0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6 +1. Make sure the Rewards Vault has a sufficient balance of the MaticX token. - transferStrategy: ITransferStrategyBase(STRATEGY_ADDRESS) // 0x53F57eAAD604307889D87b747Fc67ea9DE430B01 + ``` + IERC20(MATIC_X_ADDRESS).balanceOf(REWARDS_VAULT) > 60000 *1e18 + ``` - rewardOracle: IEACAggregatorProxy(MaticX_ORACLE_ADDRESS) // 0x5d37E4b374E6907de8Fc7fb33EE3b0af403C7403 +2. Do an ERC-20 approval from the MaticX token from the Rewards Vault to the transfer strategy contract for the total amount. - }]) - ``` + ``` + IERC20(MATIC_X_ADDRESS).approve(TRANSFER_STRATEGY_ADDRESS, 60000 *1e18); + ``` -## How to modify emissions of the LM program? +3. Create a payload with the call of EMISSION_MANAGER. -The function `_getEmissionsPerAsset()` on [EmissionTestOpOptimism.t.sol](./tests/EmissionTestOpOptimism.t.sol) defines the exact emissions for the particular case of $OP as reward token and a total distribution of 5'000'000 $OP during exactly 90 days. -The emissions can be modified there, with the only requirement being that `sum(all-emissions) == TOTAL_DISTRIBUTION` + ``` + bytes memory payload = abi.encodeWithSelector(EMISSION_MANAGER.configureAssets.selector, [{ -You can run the test via `forge test -vv` which will emit the selector encoded calldata for `configureAssets` on the emission admin which you can use to execute the configuration changes e.g. via Safe. + emissionPerSecond: 60000 * 1e18 / (180 days in seconds) -_Note: The test example above uses total distribution and duration distribution just for convenience to define emissions per second, in reality as we only pass emissions per second to `configureAssets()` we could define it in any way we wish._ + totalSupply: 0 -## How to configure emissions after the LM program has been created? + distributionEnd: current timestamp + (180 days in seconds) -After the LM program has been created, the emissions per second and the distribution end could be changed later on by the emissions admin to reduce the LM rewards or change the end date for the distribution. This can be done by calling `setEmissionPerSecond()` and `setDistributionEnd()` on the Emission Manager contract. The test examples on [EmissionConfigurationTestMATICXPolygon.t.sol](./tests/EmissionConfigurationTestMATICXPolygon.t.sol) shows how to do so. + asset: Aave Variable Debt Token of wMatic // 0x4a1c3aD6Ed28a636ee1751C69071f6be75DEb8B8 -The function `_getNewEmissionPerSecond()` and `_getNewDistributionEnd()` defines the new emissions per second and new distribution end for the particular case, which could be modified there to change to modified emissions per second and distribution end. + reward: MaticX Token address // 0xfa68FB4628DFF1028CFEc22b4162FCcd0d45efb6 -Similarly you can also run the test via `forge test -vv` which will emit the selector encoded calldata for `setEmissionPerSecond` and `setDistributionEnd` which can be used to make the configuration changes. + transferStrategy: ITransferStrategyBase(STRATEGY_ADDRESS) // 0x53F57eAAD604307889D87b747Fc67ea9DE430B01 + + rewardOracle: IEACAggregatorProxy(MaticX_ORACLE_ADDRESS) // 0x5d37E4b374E6907de8Fc7fb33EE3b0af403C7403 + + }]) + ``` + +4. Submit a proposal in the Permissioned Payloads Controller with the Payloads Manager: + + ``` + PERMISSIONED_PAYLOADS_CONTROLLER.createPayload([{ + + target: EMISSION_MANAGER, + + withDelegateCall: false, + + accessLevel: PayloadsControllerUtils.AccessControl.Level_1, + + value: 0, + + signature: "", + + calldata: payload + + }]) + ``` + +## How to update an existing LM program + +The process to update the existing LM program is quite similar to the explained LM setup process. The difference from the previous process is that you need to use the `setDistributionEnd()` and `setEmissionPerSecond()` functions instead of the `configureAssets()` function of the Emission Manager. Payloads received after encoding calls of these functions in the same way need to be passed to the `createPayload()` function of the Permissioned Payloads Controller to create a proposal. + +## Configuration tools + +This repository includes a generator to help you bootstrap the required files for an emission configuration. To generate an LM configuration proposal, you need to run `npm run generate`. It can generate either LM setup or LM update proposal. + +As a result you'll receive helper files similar to [PermissionedControllerEmissionTestMATICXPolygon](tests/PermissionedControllerEmissionTestMATICXPolygon.t.sol) and [PermissionedPayloadsControllerAndExecutorDeploy](scripts/EmissionMATICXPolygonConfigurationDeploy.s.sol). The first file is test file, it can help you to validate your configuration. Once the configuration is validated you can deploy your proposal using the second file. All required scripts you'll find inside these files. + +To get a full list of available commands, run `npm run generate -- --help`. ## FAQ's: - Do we need to have and approve the whole liquidity mining reward initially? - It is generally advisable to have and approve funds for the duration of the next 3 months of the Liquidity Mining Program. However it is the choice of the Emission Admin to do it progressively as well, as the users accrue rewards over time. + It is generally advisable to have and approve funds for the duration of the next 3 months of the Liquidity Mining Program. However it is the choice of the Payloads Manager to do it progressively as well, as the users accrue rewards over time. - Can we configure mutiple rewards for the same asset? @@ -127,13 +166,13 @@ Similarly you can also run the test via `forge test -vv` which will emit the sel - Can we stop the liquidity mining program at any time? - Yes, the liquidity mining program could be stopped at any moment by the Emission Admin. - The duration of the Liquidity Mining program could be increased as well, totally the choice of Emission Admin. + Yes, the liquidity mining program could be stopped at any moment by the Payloads Manager. + The duration of the Liquidity Mining program could be increased as well, totally the choice of Payloads Manager. To stop the liquidity mining, we can either set the emissions per second to 0 or set the distribution end to the block we wish to stop liquiditiy mining at. -- Can we change the amount of liquidty mining rewards? +- Can we change the amount of liquidity mining rewards? - Yes, the liquidity mining rewards could be increased or decreased by the Emission Admin. To do so, please refer + Yes, the liquidity mining rewards could be increased or decreased. To do so, please refer [here](https://github.com/bgd-labs/example-liquidity-mining-aave-v3/tree/feat/configure-emissions#how-to-configure-emissions-after-the-lm-program-has-been-created) ### Setup diff --git a/foundry.toml b/foundry.toml index 7c2a9fa..47b893d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,8 +5,9 @@ script = 'scripts' out = 'out' libs = ['lib'] fs_permissions = [{access = "write", path = "./reports"}] -solc = '0.8.19' +solc = '0.8.20' evm_version = 'shanghai' +no_match_path = "*.s.sol" [rpc_endpoints] mainnet = "${RPC_MAINNET}" diff --git a/generator/common.ts b/generator/common.ts index 9717d66..ad3aa27 100644 --- a/generator/common.ts +++ b/generator/common.ts @@ -158,6 +158,14 @@ export function generateContractName(options: Options, pool?: PoolIdentifier) { return name; } +export function generateScriptName(options: Options, pool: PoolIdentifier) { + let name = `${pool}_` + name += `DeployPayload`; + name += `${options.shortName}`; + name += `_${options.date}`; + return name; +} + export function getChainAlias(chain) { return chain === 'Ethereum' ? 'mainnet' : chain.toLowerCase(); } diff --git a/generator/features/setupLiquidityMining.ts b/generator/features/setupLiquidityMining.ts index b499e5b..27d63a5 100644 --- a/generator/features/setupLiquidityMining.ts +++ b/generator/features/setupLiquidityMining.ts @@ -47,10 +47,6 @@ export async function fetchLiquidityMiningSetupParams({pool}): Promise = { const response: CodeArtifact = { code: { constants: [ - cfg.rewardToken.includes('0x') - ? `address public constant override REWARD_ASSET = ${cfg.rewardToken};` - : `address public constant override REWARD_ASSET = ${pool}Assets.${cfg.rewardToken}_UNDERLYING;`, + `address public constant override REWARD_ASSET = ${cfg.rewardToken};`, `uint88 constant DURATION_DISTRIBUTION = ${cfg.distributionEnd} days;`, `uint256 public constant override TOTAL_DISTRIBUTION = ${cfg.totalReward} * 10 ** ${cfg.rewardTokenDecimals};`, - `address constant EMISSION_ADMIN = ${cfg.emissionsAdmin};\n`, `address public constant override DEFAULT_INCENTIVES_CONTROLLER = ${pool}.DEFAULT_INCENTIVES_CONTROLLER;\n`, + `IPermissionedPayloadsController public constant PAYLOADS_CONTROLLER = ${pool}.PAYLOADS_CONTROLLER;`, `ITransferStrategyBase public constant override TRANSFER_STRATEGY = ITransferStrategyBase(${cfg.transferStrategy});\n`, `IEACAggregatorProxy public constant override REWARD_ORACLE = IEACAggregatorProxy(${cfg.rewardOracle});\n`, ...cfg.assets.map((asset, index) => { @@ -143,18 +136,38 @@ export const setupLiquidityMining: FeatureModule = { }), ], fn: [ + ` - function test_activation() public { - vm.prank(EMISSION_ADMIN); - IEmissionManager(${pool}.EMISSION_MANAGER).configureAssets(_getAssetConfigs()); - - emit log_named_bytes( - 'calldata to submit from Gnosis Safe', - abi.encodeWithSelector( - IEmissionManager(${pool}.EMISSION_MANAGER).configureAssets.selector, - _getAssetConfigs() - ) + function buildActions() public view returns (IPayloadsControllerCore.ExecutionAction[] memory) { + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0].target = ${pool}.EMISSION_MANAGER; + actions[0].accessLevel = PayloadsControllerUtils.AccessControl.Level_1; + actions[0].callData = abi.encodeWithSelector( + IEmissionManager.configureAssets.selector, + _getAssetConfigs() ); + return actions; + } + + function test_activation() public { + address payloadsManager = PAYLOADS_CONTROLLER.payloadsManager(); + + IPayloadsControllerCore.ExecutionAction[] memory actions = buildActions(); + + uint40 initialTimestamp = uint40(block.timestamp); + uint40 delay = PAYLOADS_CONTROLLER + .getExecutorSettingsByAccessControl(PayloadsControllerUtils.AccessControl.Level_1) + .delay; + + // solium-disable-next-line + vm.warp(initialTimestamp - delay - 1); + vm.prank(payloadsManager); + uint40 payloadId = PAYLOADS_CONTROLLER.createPayload(actions); + // solium-disable-next-line + vm.warp(initialTimestamp); + + PAYLOADS_CONTROLLER.executePayload(payloadId); ${cfg.assets .map( diff --git a/generator/features/types.ts b/generator/features/types.ts index db36184..f12fb25 100644 --- a/generator/features/types.ts +++ b/generator/features/types.ts @@ -1,7 +1,6 @@ import {Hex} from 'viem'; export interface LiquidityMiningSetup { - emissionsAdmin: Hex; rewardToken: string; rewardTokenDecimals: number; rewardOracle: string; diff --git a/generator/features/updateLiquidityMining.ts b/generator/features/updateLiquidityMining.ts index f4c946b..62a58d8 100644 --- a/generator/features/updateLiquidityMining.ts +++ b/generator/features/updateLiquidityMining.ts @@ -114,8 +114,9 @@ export const updateLiquidityMining: FeatureModule = { constants: [ `address public constant override REWARD_ASSET = ${cfg.rewardToken};`, `uint256 public constant override NEW_TOTAL_DISTRIBUTION = ${cfg.rewardAmount} * 10 ** ${cfg.rewardTokenDecimals};`, - `address public constant override EMISSION_ADMIN = ${cfg.emissionsAdmin};`, `address public constant override EMISSION_MANAGER = ${pool}.EMISSION_MANAGER;`, + `IPermissionedPayloadsController public constant PAYLOADS_CONTROLLER = ${pool}.PAYLOADS_CONTROLLER;`, + `address public override EMISSION_ADMIN = ${cfg.emissionsAdmin};`, `uint256 public constant NEW_DURATION_DISTRIBUTION_END = ${cfg.distributionEnd} days;`, `address public constant ${translateSupplyBorrowAssetToWhaleConstant( cfg.asset, @@ -125,22 +126,62 @@ export const updateLiquidityMining: FeatureModule = { ], fn: [ ` - function test_claimRewards() public { + function buildActions() public view returns (IPayloadsControllerCore.ExecutionAction[] memory) { NewEmissionPerAsset memory newEmissionPerAsset = _getNewEmissionPerSecond(); NewDistributionEndPerAsset memory newDistributionEndPerAsset = _getNewDistributionEnd(); - vm.startPrank(EMISSION_ADMIN); - IEmissionManager(${pool}.EMISSION_MANAGER).setEmissionPerSecond( + bytes memory newEmissionPerAssetUpdatePayload = abi.encodeWithSelector( + IEmissionManager.setEmissionPerSecond.selector, newEmissionPerAsset.asset, newEmissionPerAsset.rewards, newEmissionPerAsset.newEmissionsPerSecond ); - IEmissionManager(${pool}.EMISSION_MANAGER).setDistributionEnd( + + bytes memory newDistributionEndPerAssetUpdatePayload = abi.encodeWithSelector( + IEmissionManager.setDistributionEnd.selector, newDistributionEndPerAsset.asset, newDistributionEndPerAsset.reward, newDistributionEndPerAsset.newDistributionEnd ); + IPayloadsControllerCore.ExecutionAction[] + memory executionActions = new IPayloadsControllerCore.ExecutionAction[](2); + + for (uint256 i = 0; i < 2; i++) { + executionActions[i] = IPayloadsControllerCore.ExecutionAction({ + target: ${pool}.EMISSION_MANAGER, + withDelegateCall: false, + accessLevel: PayloadsControllerUtils.AccessControl.Level_1, + value: 0, + signature: '', + callData: i == 0 + ? newEmissionPerAssetUpdatePayload + : newDistributionEndPerAssetUpdatePayload + }); + } + + return executionActions; + } + + function test_claimRewards() public { + address payloadsManager = PAYLOADS_CONTROLLER.payloadsManager(); + + IPayloadsControllerCore.ExecutionAction[] memory actions = buildActions(); + + uint40 initialTimestamp = uint40(block.timestamp); + uint40 delay = PAYLOADS_CONTROLLER + .getExecutorSettingsByAccessControl(PayloadsControllerUtils.AccessControl.Level_1) + .delay; + + // solium-disable-next-line + vm.warp(initialTimestamp - delay - 1); + vm.prank(payloadsManager); + uint40 payloadId = PAYLOADS_CONTROLLER.createPayload(actions); + // solium-disable-next-line + vm.warp(initialTimestamp); + + PAYLOADS_CONTROLLER.executePayload(payloadId); + _testClaimRewardsForWhale( ${translateSupplyBorrowAssetToWhaleConstant(cfg.asset, pool)}, ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, diff --git a/generator/generator.ts b/generator/generator.ts index fb56947..498a33b 100644 --- a/generator/generator.ts +++ b/generator/generator.ts @@ -1,8 +1,9 @@ import fs from 'fs'; import path from 'path'; -import {generateContractName, generateFolderName} from './common'; +import {generateContractName, generateFolderName, generateScriptName, getPoolChain} from './common'; import {liquidityMiningSetupTemplate} from './templates/liquiditymining.setup.template'; import {liquidityMiningUpdateTemplate} from './templates/liquiditymining.update.template'; +import {liquidityMiningPayloadDeploymentTemplate} from './templates/liquiditymining.payloaddeployment.template'; import {confirm} from '@inquirer/prompts'; import {ConfigFile, Options, PoolConfigs, PoolIdentifier, Files} from './types'; import prettier from 'prettier'; @@ -29,8 +30,8 @@ export async function generateFiles(options: Options, poolConfigs: PoolConfigs): {...prettierTsCfg, filepath: 'foo.ts'} ); + const contractName = generateContractName(options, options.pool); async function createPayloadTest(options: Options, pool: PoolIdentifier) { - const contractName = generateContractName(options, pool); const isLMSetup = options.feature == 'SETUP_LM'; return { @@ -48,9 +49,22 @@ export async function generateFiles(options: Options, poolConfigs: PoolConfigs): }; } + async function createPayloadDeployment(options: Options, pool: PoolIdentifier) { + const scriptName = generateScriptName(options, pool); + const code = liquidityMiningPayloadDeploymentTemplate(options); + return { + payloadDeployment: await prettier.format(code, { + ...prettierSolCfg, + filepath: 'foo.sol', + }), + scriptName: scriptName, + }; + } + return { jsonConfig, payloadTest: await createPayloadTest(options, options.pool), + payloadDeployment: await createPayloadDeployment(options, options.pool), }; } @@ -71,7 +85,10 @@ async function askBeforeWrite(options: Options, path: string, content: string) { /** * Writes the files according to defined folder/file format */ -export async function writeFiles(options: Options, {jsonConfig, payloadTest}: Files) { +export async function writeFiles( + options: Options, + {jsonConfig, payloadTest, payloadDeployment}: Files +) { const baseName = generateFolderName(options); const baseFolder = path.join(process.cwd(), 'tests', baseName); @@ -95,4 +112,10 @@ export async function writeFiles(options: Options, {jsonConfig, payloadTest}: Fi path.join(baseFolder, `${payloadTest.contractName}.t.sol`), payloadTest.payloadTest ); + // write payloadDeployment + await askBeforeWrite( + options, + path.join(baseFolder, `${payloadDeployment.scriptName}.s.sol`), + payloadDeployment.payloadDeployment + ); } diff --git a/generator/templates/liquiditymining.payloaddeployment.template.ts b/generator/templates/liquiditymining.payloaddeployment.template.ts new file mode 100644 index 0000000..4f112d8 --- /dev/null +++ b/generator/templates/liquiditymining.payloaddeployment.template.ts @@ -0,0 +1,41 @@ +import { CHAIN_TO_CHAIN_ID, generateContractName, generateFolderName, generateScriptName, getChainAlias, getPoolChain } from "../common"; +import { Options } from "../types"; + +export const liquidityMiningPayloadDeploymentTemplate = ( + options: Options +) => { + const testContractName = generateContractName(options, options.pool); + const poolChain = getPoolChain(options.pool); + const scriptName = generateScriptName(options, options.pool); + const chainAlias = getChainAlias(poolChain); + const folderName = generateFolderName(options); + + return` + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.0; + + +import {${testContractName}, IPermissionedPayloadsController} + from './${testContractName}.t.sol'; +import {${poolChain}Script} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; + +/** + * @dev Deploy ${poolChain} + * deploy-command: + * make deploy-ledger contract=tests/${folderName}/${scriptName}.s.sol:${scriptName} chain=${chainAlias} + * or + * make deploy-private-key contract=tests/${folderName}/${scriptName}.s.sol:${scriptName} chain=${chainAlias} private_key=$\{PRIVATE_KEY\} + */ +contract ${scriptName} is ${testContractName}, ${poolChain}Script { + // solium-disable-next-line + function setUp() public override {} + + function run() public { + IPermissionedPayloadsController.ExecutionAction[] memory actions = buildActions(); + vm.startBroadcast(); + PAYLOADS_CONTROLLER.createPayload(actions); + vm.stopBroadcast(); + } +} +`; +}; diff --git a/generator/templates/liquiditymining.setup.template.ts b/generator/templates/liquiditymining.setup.template.ts index 9af3f07..23aa2e9 100644 --- a/generator/templates/liquiditymining.setup.template.ts +++ b/generator/templates/liquiditymining.setup.template.ts @@ -22,10 +22,15 @@ export const liquidityMiningSetupTemplate = ( .filter((f) => f !== undefined) .join('\n'); - const contract = `contract ${contractName} is LMSetupBaseTest { + const contract = ` + /** + * @dev Test for ${contractName} + * command: forge test --mc ${contractName} -vv + */ + contract ${contractName} is LMSetupBaseTest { ${constants} - function setUp() public { + function setUp() public virtual { vm.createSelectFork(vm.rpcUrl('${getChainAlias(chain)}'), ${poolConfig.cache.blockNumber}); } diff --git a/generator/templates/liquiditymining.update.template.ts b/generator/templates/liquiditymining.update.template.ts index 7f31edf..e948c26 100644 --- a/generator/templates/liquiditymining.update.template.ts +++ b/generator/templates/liquiditymining.update.template.ts @@ -1,4 +1,4 @@ -import {generateContractName, getPoolChain, getChainAlias} from '../common'; +import {generateContractName, getPoolChain, getChainAlias, generateFolderName} from '../common'; import {Options, PoolConfig, PoolIdentifier} from '../types'; import {prefixWithImports} from '../utils/importsResolver'; import {prefixWithPragma} from '../utils/constants'; @@ -22,15 +22,20 @@ export const liquidityMiningUpdateTemplate = ( .filter((f) => f !== undefined) .join('\n'); - const contract = `contract ${contractName} is LMUpdateBaseTest { + const contract = ` + /** + * @dev Test for ${contractName} + * command: forge test --mc ${contractName} -vv + */ + contract ${contractName} is LMUpdateBaseTest { ${constants} - function setUp() public { - vm.createSelectFork(vm.rpcUrl('${getChainAlias(chain)}'), ${poolConfig.cache.blockNumber}); + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('${getChainAlias(chain)}'), ${poolConfig.cache.blockNumber}); } ${functions} - }`; + }`; return prefixWithPragma(prefixWithImports(contract)); }; diff --git a/generator/types.ts b/generator/types.ts index bee2341..df8ed8f 100644 --- a/generator/types.ts +++ b/generator/types.ts @@ -73,4 +73,5 @@ export interface PoolConfig { export type Files = { jsonConfig: string; payloadTest: {pool: PoolIdentifier; payloadTest: string; contractName: string}; + payloadDeployment: {payloadDeployment: string, scriptName: string}; }; diff --git a/generator/utils/importsResolver.ts b/generator/utils/importsResolver.ts index 88376b3..1296545 100644 --- a/generator/utils/importsResolver.ts +++ b/generator/utils/importsResolver.ts @@ -54,6 +54,9 @@ export function prefixWithImports(code: string) { if (findMatch(code, 'IAaveIncentivesController')) { imports += `import {IAaveIncentivesController} from '../src/interfaces/IAaveIncentivesController.sol';\n`; } + if (findMatch(code, 'IPermissionedPayloadsController')) { + imports += `import {IPermissionedPayloadsController, PayloadsControllerUtils, IPayloadsControllerCore} from 'aave-address-book/governance-v3/IPermissionedPayloadsController.sol';\n`; + } return imports + code; } diff --git a/lib/aave-address-book b/lib/aave-address-book index c9ab73b..5a3be49 160000 --- a/lib/aave-address-book +++ b/lib/aave-address-book @@ -1 +1 @@ -Subproject commit c9ab73be76dd4f5f8684a2cdd6614acaae7856f4 +Subproject commit 5a3be49ddebd79b9fb92c3f795b9fc86559ee558 diff --git a/lib/aave-governance-v3 b/lib/aave-governance-v3 new file mode 160000 index 0000000..2dfae9c --- /dev/null +++ b/lib/aave-governance-v3 @@ -0,0 +1 @@ +Subproject commit 2dfae9ce27a743979f18e0db98511bf469fd0330 diff --git a/proposal_process.png b/proposal_process.png new file mode 100644 index 0000000..a47835c Binary files /dev/null and b/proposal_process.png differ diff --git a/remappings.txt b/remappings.txt index 1dcbe46..fb1c03f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1 +1,5 @@ aave-v3-origin/=lib/aave-address-book/lib/aave-v3-origin/src/ +aave-governance-v3/=lib/aave-governance-v3/src/ +solidity-utils/=lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/src/ +openzeppelin-contracts/=lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/ +lib/aave-governance-v3:solidity-utils/=lib/aave-governance-v3/lib/solidity-utils/src diff --git a/tests/emission_maticx_polygon_configuration/EmissionMATICXPolygonConfigurationDeploy.s.sol b/tests/emission_maticx_polygon_configuration/EmissionMATICXPolygonConfigurationDeploy.s.sol new file mode 100644 index 0000000..8bb53f1 --- /dev/null +++ b/tests/emission_maticx_polygon_configuration/EmissionMATICXPolygonConfigurationDeploy.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPermissionedPayloadsController} from 'aave-address-book/governance-v3/IPermissionedPayloadsController.sol'; +import {EmissionTestMATICXPolygon} from './EmissionTestMATICXPolygon.t.sol'; +import {Script} from 'forge-std/Script.sol'; + +/** + * @dev Deploy Polygon + * deploy-command: make deploy-ledger contract=scripts/EmissionMATICXPolygonConfigurationDeploy.s.sol chain=polygon + */ + +/** + * @dev Deploy Polygon + * deploy-command: + * make deploy-ledger contract=scripts/EmissionMATICXPolygonConfigurationDeploy.s.sol chain=polygon + * or + * make deploy-private-key contract=scripts/EmissionMATICXPolygonConfigurationDeploy.s.sol chain=polygon private_key=$\{PRIVATE_KEY\} + */ +contract EmissionMATICXPolygonConfigurationDeploy is + EmissionTestMATICXPolygon, + Script +{ + // solium-disable-next-line + function setUp() public override {} + + function run() public { + IPermissionedPayloadsController.ExecutionAction[] memory actions = buildActions(); + vm.startBroadcast(); + PAYLOADS_CONTROLLER.createPayload(actions); + vm.stopBroadcast(); + } +} diff --git a/tests/emission_maticx_polygon_configuration/EmissionTestMATICXPolygon.t.sol b/tests/emission_maticx_polygon_configuration/EmissionTestMATICXPolygon.t.sol new file mode 100644 index 0000000..1e7fcc9 --- /dev/null +++ b/tests/emission_maticx_polygon_configuration/EmissionTestMATICXPolygon.t.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3Polygon, AaveV3PolygonAssets} from 'aave-address-book/AaveV3Polygon.sol'; +import {IEmissionManager, ITransferStrategyBase, RewardsDataTypes, IEACAggregatorProxy} from '../../src/interfaces/IEmissionManager.sol'; +import {LMSetupBaseTest} from '../utils/LMSetupBaseTest.sol'; +import {IPermissionedPayloadsController, PayloadsControllerUtils, IPayloadsControllerCore} from 'aave-address-book/governance-v3/IPermissionedPayloadsController.sol'; + +// TEMPORARY IMPORTS +import {IOwnable} from 'aave-address-book/common/IOwnable.sol'; +import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; +import {ProxyAdmin} from 'solidity-utils/contracts/transparent-proxy/ProxyAdmin.sol'; +import {Executor} from 'aave-governance-v3/contracts/payloads/Executor.sol'; +import {PermissionedPayloadsController} from 'aave-governance-v3/contracts/payloads/PermissionedPayloadsController.sol'; +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; + +contract EmissionTestMATICXPolygon is LMSetupBaseTest { + address public constant override REWARD_ASSET = AaveV3PolygonAssets.MaticX_UNDERLYING; + uint88 constant DURATION_DISTRIBUTION = 180 days; + uint256 public constant override TOTAL_DISTRIBUTION = 60000 * 10 ** 18; + address public EMISSION_ADMIN; + address public constant override DEFAULT_INCENTIVES_CONTROLLER = + AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER; + + // temp non const + IPermissionedPayloadsController internal PAYLOADS_CONTROLLER; + + ITransferStrategyBase public constant override TRANSFER_STRATEGY = + ITransferStrategyBase(0x53F57eAAD604307889D87b747Fc67ea9DE430B01); + + IEACAggregatorProxy public constant override REWARD_ORACLE = + IEACAggregatorProxy(AaveV3PolygonAssets.MaticX_ORACLE); + + address constant vMaticX_WHALE = 0xd0F7cB3Bf8560b1D8E20792A79F4D3aD5406014e; + + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('polygon'), 60952423); + + tempFunctionality(); + } + + function buildActions() public view returns (IPayloadsControllerCore.ExecutionAction[] memory) { + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0].target = AaveV3Polygon.EMISSION_MANAGER; + actions[0].accessLevel = PayloadsControllerUtils.AccessControl.Level_1; + actions[0].callData = abi.encodeWithSelector( + IEmissionManager.configureAssets.selector, + _getAssetConfigs() + ); + return actions; + } + + function test_activation() public { + address payloadsManager = PAYLOADS_CONTROLLER.payloadsManager(); + + IPayloadsControllerCore.ExecutionAction[] memory actions = buildActions(); + + uint40 initialTimestamp = uint40(block.timestamp); + uint40 delay = PAYLOADS_CONTROLLER + .getExecutorSettingsByAccessControl(PayloadsControllerUtils.AccessControl.Level_1) + .delay; + + // solium-disable-next-line + vm.warp(initialTimestamp - delay - 1); + vm.prank(payloadsManager); + uint40 payloadId = PAYLOADS_CONTROLLER.createPayload(actions); + // solium-disable-next-line + vm.warp(initialTimestamp); + + PAYLOADS_CONTROLLER.executePayload(payloadId); + + _testClaimRewardsForWhale( + vMaticX_WHALE, + AaveV3PolygonAssets.WPOL_V_TOKEN, + DURATION_DISTRIBUTION, + 7150 * 10 ** 18 + ); + } + + function _getAssetConfigs() + internal + view + override + returns (RewardsDataTypes.RewardsConfigInput[] memory) + { + uint32 distributionEnd = uint32(block.timestamp + DURATION_DISTRIBUTION); + + EmissionPerAsset[] memory emissionsPerAsset = _getEmissionsPerAsset(); + + RewardsDataTypes.RewardsConfigInput[] + memory configs = new RewardsDataTypes.RewardsConfigInput[](emissionsPerAsset.length); + for (uint256 i = 0; i < emissionsPerAsset.length; i++) { + configs[i] = RewardsDataTypes.RewardsConfigInput({ + emissionPerSecond: _toUint88(emissionsPerAsset[i].emission / DURATION_DISTRIBUTION), + totalSupply: 0, // IMPORTANT this will not be taken into account by the contracts, so 0 is fine + distributionEnd: distributionEnd, + asset: emissionsPerAsset[i].asset, + reward: REWARD_ASSET, + transferStrategy: TRANSFER_STRATEGY, + rewardOracle: REWARD_ORACLE + }); + } + + return configs; + } + + function _getEmissionsPerAsset() internal pure override returns (EmissionPerAsset[] memory) { + EmissionPerAsset[] memory emissionsPerAsset = new EmissionPerAsset[](1); + + emissionsPerAsset[0] = EmissionPerAsset({ + asset: AaveV3PolygonAssets.WPOL_V_TOKEN, + emission: 60000 * 10 ** 18 + }); + + uint256 totalDistribution; + for (uint256 i = 0; i < emissionsPerAsset.length; i++) { + totalDistribution += emissionsPerAsset[i].emission; + } + require(totalDistribution == TOTAL_DISTRIBUTION, 'INVALID_SUM_OF_EMISSIONS'); + + return emissionsPerAsset; + } + + // todo: remove + function tempFunctionality() internal { + Executor executor = new Executor(); + PermissionedPayloadsController permissionedPayloadsControllerImpl = new PermissionedPayloadsController(); + + IPayloadsControllerCore.UpdateExecutorInput[] + memory executorInput = new IPayloadsControllerCore.UpdateExecutorInput[](1); + executorInput[0].accessLevel = PayloadsControllerUtils.AccessControl.Level_1; + executorInput[0].executorConfig.executor = address(executor); + executorInput[0].executorConfig.delay = 1 days; + + TransparentProxyFactory proxyFactory = new TransparentProxyFactory(); + PAYLOADS_CONTROLLER = IPermissionedPayloadsController( + proxyFactory.create( + address(permissionedPayloadsControllerImpl), + ProxyAdmin(address(728)), + abi.encodeWithSelector( + IPermissionedPayloadsController.initialize.selector, + address(490), + address(659), + executorInput + ) + ) + ); + + address emissionManagerOwner = IOwnable(AaveV3Polygon.EMISSION_MANAGER).owner(); + vm.prank(emissionManagerOwner); + IEmissionManager(AaveV3Polygon.EMISSION_MANAGER).setEmissionAdmin( + REWARD_ASSET, + address(executor) + ); + EMISSION_ADMIN = address(executor); + + IOwnable(address(executor)).transferOwnership(address(PAYLOADS_CONTROLLER)); + + address rewardsVault = TRANSFER_STRATEGY.getRewardsVault(); + deal(REWARD_ASSET, rewardsVault, TOTAL_DISTRIBUTION); + + vm.prank(rewardsVault); + IERC20(REWARD_ASSET).approve(address(TRANSFER_STRATEGY), TOTAL_DISTRIBUTION); + } +}