Skip to content

Commit

Permalink
feat: added example test for stMATIC and MATICX rewards on vWMATIC, A…
Browse files Browse the repository at this point in the history
…ave v3 Polygon (#3)

* feat: added example test for stMATIC rewards on vWMATIC on Aave v3 Polygon

* feat: added example for MATICX rewards

* removed unused constant on RewardsConfigHelpers
  • Loading branch information
eboadom authored Feb 16, 2023
1 parent cd8398c commit 6d6189a
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 3 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ update:; forge update
build :; forge build --sizes --via-ir
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

# 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
deploy-stmatic-transfer-strategy :; forge script scripts/RewardsConfigHelpers.s.sol:STMATICDeployTransferStrategy --rpc-url polygon --broadcast --legacy --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv
24 changes: 22 additions & 2 deletions scripts/RewardsConfigHelpers.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,35 @@ pragma solidity ^0.8.0;

import {Script} from 'forge-std/Script.sol';
import {PullRewardsTransferStrategy} from 'aave-v3-periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol';
import {AaveV3Polygon} from 'aave-address-book/AaveV3Polygon.sol';

contract SDDeployTransferStrategy is Script {
address internal constant REWARDS_CONTROLLER = 0x929EC64c34a17401F460460D4B9390518E5B473e;
address internal constant EMISSION_ADMIN = 0x51358004cFe135E64453d7F6a0dC433CAba09A2a;
address internal constant REWARDS_VAULT = EMISSION_ADMIN;

function run() external {
vm.startBroadcast();
new PullRewardsTransferStrategy(REWARDS_CONTROLLER, EMISSION_ADMIN, REWARDS_VAULT);
new PullRewardsTransferStrategy(
AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER,
EMISSION_ADMIN,
REWARDS_VAULT
);
vm.stopBroadcast();
}
}

/// @dev same to be used for MATICX, as they share rewards vault and emission admin
contract STMATICDeployTransferStrategy is Script {
address internal constant REWARDS_VAULT = EMISSION_ADMIN;
address internal constant EMISSION_ADMIN = 0x0c54a0BCCF5079478a144dBae1AFcb4FEdf7b263;

function run() external {
vm.startBroadcast();
new PullRewardsTransferStrategy(
AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER,
EMISSION_ADMIN,
REWARDS_VAULT
);
vm.stopBroadcast();
}
}
141 changes: 141 additions & 0 deletions tests/EmissionTestMATICXPolygon.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {Test} from 'forge-std/Test.sol';
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
import {AaveV3Polygon, AaveV3PolygonAssets} from 'aave-address-book/AaveV3Polygon.sol';
import {IAaveIncentivesController} from '../src/interfaces/IAaveIncentivesController.sol';

import {IEmissionManager, ITransferStrategyBase, RewardsDataTypes, IEACAggregatorProxy} from '../src/interfaces/IEmissionManager.sol';
import {BaseTest} from './utils/BaseTest.sol';

contract EmissionTestMATICXPolygon is BaseTest {
/// @dev Used to simplify the definition of a program of emissions
/// @param asset The asset on which to put reward on, usually Aave aTokens or vTokens (variable debt tokens)
/// @param emission Total emission of a `reward` token during the whole distribution duration defined
/// E.g. With an emission of 10_000 MATICX tokens during 1 month, an emission of 50% for variableDebtPolWMATIC would be
/// 10_000 * 1e18 * 50% / 30 days in seconds = 1_000 * 1e18 / 2_592_000 = ~ 0.0003858 * 1e18 MATICX per second
struct EmissionPerAsset {
address asset;
uint256 emission;
}

address constant EMISSION_ADMIN = 0x0c54a0BCCF5079478a144dBae1AFcb4FEdf7b263; // Polygon Foundation
address constant REWARD_ASSET = AaveV3PolygonAssets.MaticX_UNDERLYING;
IEACAggregatorProxy constant REWARD_ORACLE =
IEACAggregatorProxy(AaveV3PolygonAssets.MaticX_ORACLE);

/// @dev already deployed and configured for the both the MATICX asset and the 0x0c54a0BCCF5079478a144dBae1AFcb4FEdf7b263
/// EMISSION_ADMIN
ITransferStrategyBase constant TRANSFER_STRATEGY =
ITransferStrategyBase(0x53F57eAAD604307889D87b747Fc67ea9DE430B01);

uint256 constant TOTAL_DISTRIBUTION = 60_000 ether; // 10'000 MATICX/month, 6 months
uint88 constant DURATION_DISTRIBUTION = 180 days;

address MATICX_WHALE = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
address vWMATIC_WHALE = 0xe52F5349153b8eb3B89675AF45aC7502C4997E6A;

function setUp() public {
vm.createSelectFork(vm.rpcUrl('polygon'), 39361970);
}

function test_activation() public {
vm.startPrank(EMISSION_ADMIN);
/// @dev IMPORTANT!!
/// The emissions admin should have REWARD_ASSET funds, and have approved the TOTAL_DISTRIBUTION
/// amount to the transfer strategy. If not, REWARDS WILL ACCRUE FINE AFTER `configureAssets()`, BUT THEY
/// WILL NOT BE CLAIMABLE UNTIL THERE IS FUNDS AND ALLOWANCE.
/// It is possible to approve less than TOTAL_DISTRIBUTION and doing it progressively over time as users
/// accrue more, but that is a decision of the emission's admin
IERC20(REWARD_ASSET).approve(address(TRANSFER_STRATEGY), TOTAL_DISTRIBUTION);

IEmissionManager(AaveV3Polygon.EMISSION_MANAGER).configureAssets(_getAssetConfigs());

emit log_named_bytes(
'calldata to submit from Gnosis Safe',
abi.encodeWithSelector(
IEmissionManager(AaveV3Polygon.EMISSION_MANAGER).configureAssets.selector,
_getAssetConfigs()
)
);

vm.stopPrank();

vm.startPrank(MATICX_WHALE);
IERC20(REWARD_ASSET).transfer(EMISSION_ADMIN, 50_000 ether);

vm.stopPrank();

vm.startPrank(vWMATIC_WHALE);

vm.warp(block.timestamp + 30 days);

address[] memory assets = new address[](1);
assets[0] = AaveV3PolygonAssets.WMATIC_V_TOKEN;

uint256 balanceBefore = IERC20(REWARD_ASSET).balanceOf(vWMATIC_WHALE);

IAaveIncentivesController(AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER).claimRewards(
assets,
type(uint256).max,
vWMATIC_WHALE,
REWARD_ASSET
);

uint256 balanceAfter = IERC20(REWARD_ASSET).balanceOf(vWMATIC_WHALE);

uint256 deviationAccepted = 1300 ether; // Approx estimated rewards with current emission in 1 month
assertApproxEqAbs(
balanceBefore,
balanceAfter,
deviationAccepted,
'Invalid delta on claimed rewards'
);

vm.stopPrank();
}

function _getAssetConfigs() internal view 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 returns (EmissionPerAsset[] memory) {
EmissionPerAsset[] memory emissionsPerAsset = new EmissionPerAsset[](1);
emissionsPerAsset[0] = EmissionPerAsset({
asset: AaveV3PolygonAssets.WMATIC_V_TOKEN,
emission: TOTAL_DISTRIBUTION // 100% of the distribution
});

uint256 totalDistribution;
for (uint256 i = 0; i < emissionsPerAsset.length; i++) {
totalDistribution += emissionsPerAsset[i].emission;
}
require(totalDistribution == TOTAL_DISTRIBUTION, 'INVALID_SUM_OF_EMISSIONS');

return emissionsPerAsset;
}

function _toUint88(uint256 value) internal pure returns (uint88) {
require(value <= type(uint88).max, "SafeCast: value doesn't fit in 88 bits");
return uint88(value);
}
}
141 changes: 141 additions & 0 deletions tests/EmissionTestSTMATICPolygon.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {Test} from 'forge-std/Test.sol';
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
import {AaveV3Polygon, AaveV3PolygonAssets} from 'aave-address-book/AaveV3Polygon.sol';
import {IAaveIncentivesController} from '../src/interfaces/IAaveIncentivesController.sol';

import {IEmissionManager, ITransferStrategyBase, RewardsDataTypes, IEACAggregatorProxy} from '../src/interfaces/IEmissionManager.sol';
import {BaseTest} from './utils/BaseTest.sol';

contract EmissionTestSTMATICPolygon is BaseTest {
/// @dev Used to simplify the definition of a program of emissions
/// @param asset The asset on which to put reward on, usually Aave aTokens or vTokens (variable debt tokens)
/// @param emission Total emission of a `reward` token during the whole distribution duration defined
/// E.g. With an emission of 10_000 stMATIC tokens during 1 month, an emission of 50% for variableDebtPolWMATIC would be
/// 10_000 * 1e18 * 50% / 30 days in seconds = 1_000 * 1e18 / 2_592_000 = ~ 0.0003858 * 1e18 stMATIC per second
struct EmissionPerAsset {
address asset;
uint256 emission;
}

address constant EMISSION_ADMIN = 0x0c54a0BCCF5079478a144dBae1AFcb4FEdf7b263; // Polygon Foundation
address constant REWARD_ASSET = AaveV3PolygonAssets.stMATIC_UNDERLYING;
IEACAggregatorProxy constant REWARD_ORACLE =
IEACAggregatorProxy(AaveV3PolygonAssets.stMATIC_ORACLE);

/// @dev already deployed and configured for the both the stMATIC asset and the 0x0c54a0BCCF5079478a144dBae1AFcb4FEdf7b263
/// EMISSION_ADMIN
ITransferStrategyBase constant TRANSFER_STRATEGY =
ITransferStrategyBase(0x53F57eAAD604307889D87b747Fc67ea9DE430B01);

uint256 constant TOTAL_DISTRIBUTION = 60_000 ether; // 10'000 stMATIC/month, 6 months
uint88 constant DURATION_DISTRIBUTION = 180 days;

address STMATIC_WHALE = 0xB975364Bf0368726075A80da76D1Bf260244a25D;
address vWMATIC_WHALE = 0xe52F5349153b8eb3B89675AF45aC7502C4997E6A;

function setUp() public {
vm.createSelectFork(vm.rpcUrl('polygon'), 39361970);
}

function test_activation() public {
vm.startPrank(EMISSION_ADMIN);
/// @dev IMPORTANT!!
/// The emissions admin should have REWARD_ASSET funds, and have approved the TOTAL_DISTRIBUTION
/// amount to the transfer strategy. If not, REWARDS WILL ACCRUE FINE AFTER `configureAssets()`, BUT THEY
/// WILL NOT BE CLAIMABLE UNTIL THERE IS FUNDS AND ALLOWANCE.
/// It is possible to approve less than TOTAL_DISTRIBUTION and doing it progressively over time as users
/// accrue more, but that is a decision of the emission's admin
IERC20(REWARD_ASSET).approve(address(TRANSFER_STRATEGY), TOTAL_DISTRIBUTION);

IEmissionManager(AaveV3Polygon.EMISSION_MANAGER).configureAssets(_getAssetConfigs());

emit log_named_bytes(
'calldata to submit from Gnosis Safe',
abi.encodeWithSelector(
IEmissionManager(AaveV3Polygon.EMISSION_MANAGER).configureAssets.selector,
_getAssetConfigs()
)
);

vm.stopPrank();

vm.startPrank(STMATIC_WHALE);
IERC20(REWARD_ASSET).transfer(EMISSION_ADMIN, 50_000 ether);

vm.stopPrank();

vm.startPrank(vWMATIC_WHALE);

vm.warp(block.timestamp + 30 days);

address[] memory assets = new address[](1);
assets[0] = AaveV3PolygonAssets.WMATIC_V_TOKEN;

uint256 balanceBefore = IERC20(REWARD_ASSET).balanceOf(vWMATIC_WHALE);

IAaveIncentivesController(AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER).claimRewards(
assets,
type(uint256).max,
vWMATIC_WHALE,
REWARD_ASSET
);

uint256 balanceAfter = IERC20(REWARD_ASSET).balanceOf(vWMATIC_WHALE);

uint256 deviationAccepted = 1300 ether; // Approx estimated rewards with current emission in 1 month
assertApproxEqAbs(
balanceBefore,
balanceAfter,
deviationAccepted,
'Invalid delta on claimed rewards'
);

vm.stopPrank();
}

function _getAssetConfigs() internal view 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 returns (EmissionPerAsset[] memory) {
EmissionPerAsset[] memory emissionsPerAsset = new EmissionPerAsset[](1);
emissionsPerAsset[0] = EmissionPerAsset({
asset: AaveV3PolygonAssets.WMATIC_V_TOKEN,
emission: TOTAL_DISTRIBUTION // 100% of the distribution
});

uint256 totalDistribution;
for (uint256 i = 0; i < emissionsPerAsset.length; i++) {
totalDistribution += emissionsPerAsset[i].emission;
}
require(totalDistribution == TOTAL_DISTRIBUTION, 'INVALID_SUM_OF_EMISSIONS');

return emissionsPerAsset;
}

function _toUint88(uint256 value) internal pure returns (uint88) {
require(value <= type(uint88).max, "SafeCast: value doesn't fit in 88 bits");
return uint88(value);
}
}

0 comments on commit 6d6189a

Please sign in to comment.