diff --git a/foundry.toml b/foundry.toml index 61253e0b..44eaccc7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ optimizer_runs = 200 solc='0.8.19' evm_version = 'paris' bytecode_hash = 'none' +ignored_warnings_from = ["src/periphery/contracts/treasury/RevenueSplitter.sol"] out = 'out' libs = ['lib'] remappings = [ diff --git a/src/deployments/contracts/procedures/AaveV3TreasuryProcedure.sol b/src/deployments/contracts/procedures/AaveV3TreasuryProcedure.sol index 8fddece8..b6610345 100644 --- a/src/deployments/contracts/procedures/AaveV3TreasuryProcedure.sol +++ b/src/deployments/contracts/procedures/AaveV3TreasuryProcedure.sol @@ -10,7 +10,6 @@ import '../../interfaces/IMarketReportTypes.sol'; contract AaveV3TreasuryProcedure { struct TreasuryReport { address treasuryImplementation; - address proxyAdmin; address treasury; } @@ -26,20 +25,12 @@ contract AaveV3TreasuryProcedure { if (salt != '') { Collector treasuryImplementation = new Collector{salt: salt}(); treasuryImplementation.initialize(address(0), 0); - treasuryReport.treasuryImplementation = address(treasuryImplementation); - if (deployedProxyAdmin == address(0)) { - treasuryReport.proxyAdmin = address(new ProxyAdmin{salt: salt}()); - IOwnable(treasuryReport.proxyAdmin).transferOwnership(treasuryOwner); - } else { - treasuryReport.proxyAdmin = deployedProxyAdmin; - } - treasuryReport.treasury = address( new TransparentUpgradeableProxy{salt: salt}( treasuryReport.treasuryImplementation, - treasuryReport.proxyAdmin, + deployedProxyAdmin, abi.encodeWithSelector( treasuryImplementation.initialize.selector, address(treasuryOwner), @@ -52,17 +43,10 @@ contract AaveV3TreasuryProcedure { treasuryImplementation.initialize(address(0), 0); treasuryReport.treasuryImplementation = address(treasuryImplementation); - if (deployedProxyAdmin == address(0)) { - treasuryReport.proxyAdmin = address(new ProxyAdmin()); - IOwnable(treasuryReport.proxyAdmin).transferOwnership(treasuryOwner); - } else { - treasuryReport.proxyAdmin = deployedProxyAdmin; - } - treasuryReport.treasury = address( new TransparentUpgradeableProxy( treasuryReport.treasuryImplementation, - treasuryReport.proxyAdmin, + deployedProxyAdmin, abi.encodeWithSelector( treasuryImplementation.initialize.selector, address(treasuryOwner), diff --git a/src/deployments/interfaces/IMarketReportTypes.sol b/src/deployments/interfaces/IMarketReportTypes.sol index 93c83f9c..52f5e72c 100644 --- a/src/deployments/interfaces/IMarketReportTypes.sol +++ b/src/deployments/interfaces/IMarketReportTypes.sol @@ -89,6 +89,7 @@ struct MarketReport { address staticATokenFactoryImplementation; address staticATokenFactoryProxy; address staticATokenImplementation; + address revenueSplitter; } struct LibrariesReport { @@ -124,6 +125,9 @@ struct MarketConfig { uint128 flashLoanPremiumTotal; uint128 flashLoanPremiumToProtocol; address incentivesProxy; + address treasury; // let empty for deployment of collector, otherwise reuse treasury address + address treasuryPartner; // let empty for single treasury, or add treasury partner for revenue split between two organizations. + uint16 treasurySplitPercent; // ignored if treasuryPartner is empty, otherwise the split percent for the first treasury (recipientA, values between 00_01 and 100_00) } struct DeployFlags { @@ -177,6 +181,7 @@ struct PeripheryReport { address treasuryImplementation; address emissionManager; address rewardsControllerImplementation; + address revenueSplitter; } struct ParaswapReport { diff --git a/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol b/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol index ec93fc80..02a0100f 100644 --- a/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol +++ b/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol @@ -170,13 +170,18 @@ library AaveV3BatchOrchestration { PeripheryReport memory peripheryReport, AaveV3TokensBatch.TokensReport memory tokensReport ) internal returns (ConfigEngineReport memory) { + address treasury = peripheryReport.treasury; + if (peripheryReport.revenueSplitter != address(0)) { + treasury = peripheryReport.revenueSplitter; + } + AaveV3HelpersBatchOne helpersBatchOne = new AaveV3HelpersBatchOne( setupReport.poolProxy, setupReport.poolConfiguratorProxy, miscReport.defaultInterestRateStrategy, peripheryReport.aaveOracle, setupReport.rewardsControllerProxy, - peripheryReport.treasury, + treasury, tokensReport.aToken, tokensReport.variableDebtToken, tokensReport.stableDebtToken @@ -328,6 +333,7 @@ library AaveV3BatchOrchestration { report.staticATokenFactoryProxy = staticATokenReport.staticATokenFactoryProxy; report.staticATokenImplementation = staticATokenReport.staticATokenImplementation; report.transparentProxyFactory = staticATokenReport.transparentProxyFactory; + report.revenueSplitter = peripheryReport.revenueSplitter; return report; } diff --git a/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol b/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol index 258d0d6c..1f3639ea 100644 --- a/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol +++ b/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol @@ -7,6 +7,9 @@ import {AaveV3IncentiveProcedure} from '../../../contracts/procedures/AaveV3Ince import {AaveV3DefaultRateStrategyProcedure} from '../../../contracts/procedures/AaveV3DefaultRateStrategyProcedure.sol'; import {IOwnable} from 'solidity-utils/contracts/transparent-proxy/interfaces/IOwnable.sol'; import '../../../interfaces/IMarketReportTypes.sol'; +import {IRewardsController} from '../../../../periphery/contracts/rewards/interfaces/IRewardsController.sol'; +import {IOwnable} from 'solidity-utils/contracts/transparent-proxy/interfaces/IOwnable.sol'; +import {RevenueSplitter} from '../../../../periphery/contracts/treasury/RevenueSplitter.sol'; contract AaveV3PeripheryBatch is AaveV3TreasuryProcedure, @@ -21,15 +24,37 @@ contract AaveV3PeripheryBatch is address poolAddressesProvider, address setupBatch ) { - TreasuryReport memory treasuryReport = _deployAaveV3Treasury( - poolAdmin, - config.proxyAdmin, - config.salt - ); + if (config.proxyAdmin == address(0)) { + _report.proxyAdmin = address(new ProxyAdmin{salt: config.salt}()); + IOwnable(_report.proxyAdmin).transferOwnership(poolAdmin); + } else { + _report.proxyAdmin = config.proxyAdmin; + } + _report.aaveOracle = _deployAaveOracle(config.oracleDecimals, poolAddressesProvider); - _report.proxyAdmin = treasuryReport.proxyAdmin; - _report.treasury = treasuryReport.treasury; - _report.treasuryImplementation = treasuryReport.treasuryImplementation; + + if (config.treasury == address(0)) { + TreasuryReport memory treasuryReport = _deployAaveV3Treasury( + poolAdmin, + _report.proxyAdmin, + config.salt + ); + + _report.treasury = treasuryReport.treasury; + _report.treasuryImplementation = treasuryReport.treasuryImplementation; + } else { + _report.treasury = config.treasury; + } + + if ( + config.treasuryPartner != address(0) && + config.treasurySplitPercent > 0 && + config.treasurySplitPercent < 100_00 + ) { + _report.revenueSplitter = address( + new RevenueSplitter(_report.treasury, config.treasuryPartner, config.treasurySplitPercent) + ); + } if (config.incentivesProxy == address(0)) { (_report.emissionManager, _report.rewardsControllerImplementation) = _deployIncentives( diff --git a/src/periphery/contracts/treasury/IRevenueSplitter.sol b/src/periphery/contracts/treasury/IRevenueSplitter.sol new file mode 100644 index 00000000..0bb36044 --- /dev/null +++ b/src/periphery/contracts/treasury/IRevenueSplitter.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; + +interface IRevenueSplitterErrors { + error InvalidPercentSplit(); +} + +/// @title IRevenueSplitter +/// @notice Interface for RevenueSplitter contract +/// @dev The `RevenueSplitter` is a state-less non-upgradeable contract that supports 2 recipients (A and B), and defines the percentage split of the recipient A, with a value between 1 and 99_99. +/// The `RevenueSplitter` contract must be attached to the `AaveV3ConfigEngine` as `treasury`, making new listings to use `RevenueSplitter` as treasury (instead of `Collector` ) at the `AToken` initialization, making all revenue managed by ATokens redirected to the RevenueSplitter contract. +/// Once parties want to share their revenue, anyone can call `function splitRevenue(IERC20[] memory tokens)` to check the accrued ERC20 balance inside this contract, and split the amounts between the two recipients. +/// It also supports split of native currency via `function splitNativeRevenue() external`, in case the instance receives native currency. +/// +/// Warning: For recipients, you can use any address, but preferable to use `Collector`, a Safe smart contract multisig or a smart contract that can handle both ERC20 and native transfers, to prevent balances to be locked. +interface IRevenueSplitter is IRevenueSplitterErrors { + /// @notice Split token balances in RevenueSplitter and transfer between two recipients + /// @param tokens List of tokens to check balance and split amounts + /// @dev Specs: + /// - Does not revert if token balance is zero (no-op). + /// - Rounds in favor of RECIPIENT_B (1 wei round). + /// - Anyone can call this function anytime. + /// - This method will always send ERC20 tokens to recipients, even if the recipients does NOT support the ERC20 interface. At deployment time is recommended to ensure both recipients can handle ERC20 and native transfers via e2e tests. + function splitRevenue(IERC20[] memory tokens) external; + + /// @notice Split native currency in RevenueSplitter and transfer between two recipients + /// @dev Specs: + /// - Does not revert if native balance is zero (no-op) + /// - Rounds in favor of RECIPIENT_B (1 wei round). + /// - Anyone can call this function anytime. + /// - This method will always send native currency to recipients, and does NOT revert if one or both recipients doesn't support handling native currency. At deployment time is recommended to ensure both recipients can handle ERC20 and native transfers via e2e tests. + /// - If one recipient can not receive native currency, repeatedly calling the function will rescue/drain the funds of the second recipient (50% per call), allowing manual recovery of funds. + function splitNativeRevenue() external; + + function RECIPIENT_A() external view returns (address payable); + + function RECIPIENT_B() external view returns (address payable); + + /// @dev Percentage of the split that goes to RECIPIENT_A, the diff goes to RECIPIENT_B, from 1 to 99_99 + function SPLIT_PERCENTAGE_RECIPIENT_A() external view returns (uint16); +} diff --git a/src/periphery/contracts/treasury/RevenueSplitter.sol b/src/periphery/contracts/treasury/RevenueSplitter.sol new file mode 100644 index 00000000..e926551a --- /dev/null +++ b/src/periphery/contracts/treasury/RevenueSplitter.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +import {IRevenueSplitter} from './IRevenueSplitter.sol'; +import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from 'aave-v3-core/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {PercentageMath} from 'aave-v3-core/contracts/protocol/libraries/math/PercentageMath.sol'; +import {ReentrancyGuard} from 'aave-v3-periphery/contracts/dependencies/openzeppelin/ReentrancyGuard.sol'; + +/** + * @title RevenueSplitter + * @author Catapulta + * @dev This periphery contract is responsible for splitting funds between two recipients. + * Replace COLLECTOR in ATokens or Debt Tokens with RevenueSplitter, and them set COLLECTORs as recipients. + */ +contract RevenueSplitter is IRevenueSplitter, ReentrancyGuard { + using GPv2SafeERC20 for IERC20; + using PercentageMath for uint256; + + address payable public immutable RECIPIENT_A; + address payable public immutable RECIPIENT_B; + + uint16 public immutable SPLIT_PERCENTAGE_RECIPIENT_A; + + constructor(address recipientA, address recipientB, uint16 splitPercentageRecipientA) { + if ( + splitPercentageRecipientA == 0 || + splitPercentageRecipientA >= PercentageMath.PERCENTAGE_FACTOR + ) { + revert InvalidPercentSplit(); + } + RECIPIENT_A = payable(recipientA); + RECIPIENT_B = payable(recipientB); + SPLIT_PERCENTAGE_RECIPIENT_A = splitPercentageRecipientA; + } + + /// @inheritdoc IRevenueSplitter + function splitRevenue(IERC20[] memory tokens) external nonReentrant { + for (uint8 x; x < tokens.length; ++x) { + uint256 balance = tokens[x].balanceOf(address(this)); + + if (balance == 0) { + continue; + } + + uint256 amount_A = balance.percentMul(SPLIT_PERCENTAGE_RECIPIENT_A); + uint256 amount_B = balance - amount_A; + + tokens[x].safeTransfer(RECIPIENT_A, amount_A); + tokens[x].safeTransfer(RECIPIENT_B, amount_B); + } + } + + /// @inheritdoc IRevenueSplitter + function splitNativeRevenue() external nonReentrant { + uint256 balance = address(this).balance; + + if (balance == 0) { + return; + } + + uint256 amount_A = balance.percentMul(SPLIT_PERCENTAGE_RECIPIENT_A); + uint256 amount_B = balance - amount_A; + + // Do not revert if fails to send to RECIPIENT_A or RECIPIENT_B, to prevent one recipient from blocking the other + // if recipient does not accept native currency via fallback function or receive. + // This can also be used as a manual recovery mechanism in case of an account does not support receiving native currency. + RECIPIENT_A.call{value: amount_A}(''); + RECIPIENT_B.call{value: amount_B}(''); + } + + receive() external payable {} +} diff --git a/tests/AaveV3BatchDeployment.t.sol b/tests/AaveV3BatchDeployment.t.sol index b6c89cc0..67f1c849 100644 --- a/tests/AaveV3BatchDeployment.t.sol +++ b/tests/AaveV3BatchDeployment.t.sol @@ -15,6 +15,10 @@ import {IAaveV3ConfigEngine} from 'aave-v3-periphery/contracts/v3-config-engine/ import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; import {AaveV3ConfigEngine} from 'aave-v3-periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; import {SequencerOracle} from 'aave-v3-core/contracts/mocks/oracle/SequencerOracle.sol'; +import {IPoolDataProvider} from 'aave-v3-core/contracts/interfaces/IPoolDataProvider.sol'; +import {IAToken} from 'aave-v3-core/contracts/interfaces/IAToken.sol'; +import {IncentivizedERC20} from 'aave-v3-core/contracts/protocol/tokenization/base/IncentivizedERC20.sol'; +import {RewardsController} from 'aave-v3-periphery/contracts/rewards/RewardsController.sol'; import {RewardsController} from 'aave-v3-periphery/contracts/rewards/RewardsController.sol'; import {EmissionManager} from 'aave-v3-periphery/contracts/rewards/EmissionManager.sol'; @@ -52,7 +56,10 @@ contract AaveV3BatchDeployment is BatchTestProcedures { address(0), 0.0005e4, 0.0004e4, - address(0) + address(0), + address(0), + address(0), + 0 ); } @@ -79,6 +86,11 @@ contract AaveV3BatchDeployment is BatchTestProcedures { manager.addPoolAdmin(address(testnetListingPayload)); testnetListingPayload.execute(); + + (address aToken, , ) = IPoolDataProvider(fullReport.protocolDataProvider) + .getReserveTokensAddresses(weth9); + + assertEq(IAToken(aToken).RESERVE_TREASURY_ADDRESS(), fullReport.treasury); } function testAaveV3L2BatchDeploymentCheck() public { @@ -131,4 +143,38 @@ contract AaveV3BatchDeployment is BatchTestProcedures { deployAaveV3Testnet(marketOwner, roles, config, flags, deployedContracts) ); } + + function testAaveV3TreasuryPartnerBatchDeploymentCheck() public { + config.treasuryPartner = makeAddr('TREASURY_PARTNER'); + config.treasurySplitPercent = 5000; + + MarketReport memory fullReport = deployAaveV3Testnet( + marketOwner, + roles, + config, + flags, + deployedContracts + ); + + checkFullReport(config, flags, fullReport); + + AaveV3TestListing testnetListingPayload = new AaveV3TestListing( + IAaveV3ConfigEngine(fullReport.configEngine), + marketOwner, + weth9, + fullReport + ); + + ACLManager manager = ACLManager(fullReport.aclManager); + + vm.prank(poolAdmin); + manager.addPoolAdmin(address(testnetListingPayload)); + + testnetListingPayload.execute(); + + (address aToken, , ) = IPoolDataProvider(fullReport.protocolDataProvider) + .getReserveTokensAddresses(weth9); + + assertEq(IAToken(aToken).RESERVE_TREASURY_ADDRESS(), fullReport.revenueSplitter); + } } diff --git a/tests/AaveV3BatchTests.t.sol b/tests/AaveV3BatchTests.t.sol index 4cf5f0f9..2597a38f 100644 --- a/tests/AaveV3BatchTests.t.sol +++ b/tests/AaveV3BatchTests.t.sol @@ -69,7 +69,10 @@ contract AaveV3BatchTests is BatchTestProcedures { address(0), 0.0005e4, 0.0004e4, - address(0) + address(0), + address(0), + address(0), + 0 ); flags = DeployFlags(false); diff --git a/tests/AaveV3PermissionsTest.t.sol b/tests/AaveV3PermissionsTest.t.sol index 4224adb2..d5408b68 100644 --- a/tests/AaveV3PermissionsTest.t.sol +++ b/tests/AaveV3PermissionsTest.t.sol @@ -12,6 +12,7 @@ import {AugustusRegistryMock} from './mocks/AugustusRegistryMock.sol'; import {MockParaSwapFeeClaimer} from '../src/periphery/contracts/mocks/swap/MockParaSwapFeeClaimer.sol'; import {WETH9} from 'aave-v3-core/contracts/dependencies/weth/WETH9.sol'; import {BatchTestProcedures} from './utils/BatchTestProcedures.sol'; +import {IRevenueSplitter} from 'aave-v3-periphery/contracts/treasury/IRevenueSplitter.sol'; contract AaveV3PermissionsTest is BatchTestProcedures { /** @@ -50,6 +51,155 @@ contract AaveV3PermissionsTest is BatchTestProcedures { deployedContracts ); + ACLManager aclManager = ACLManager( + IPoolAddressesProvider(report.poolAddressesProvider).getACLManager() + ); + { + address providerOwner = Ownable(report.poolAddressesProvider).owner(); + assertEq( + providerOwner, + roles.marketOwner, + 'PoolAddressesProvider owner must be roles.marketOwner' + ); + } + { + address providerRegistryOwner = Ownable(report.poolAddressesProviderRegistry).owner(); + assertEq( + providerRegistryOwner, + roles.marketOwner, + 'PoolAddressesProviderRegistry owner must be roles.marketOwner' + ); + } + { + address providerAclAdmin = IPoolAddressesProvider(report.poolAddressesProvider).getACLAdmin(); + assertEq( + providerAclAdmin, + roles.poolAdmin, + 'PoolAddressesProvider.getACLAdmin() must be pool admin' + ); + } + { + bool isPoolAdminDefaultAdmin = aclManager.hasRole(emptyBytes, roles.poolAdmin); + assertTrue(isPoolAdminDefaultAdmin, 'roles.PoolAdmin must be default admin'); + } + { + bool isPoolAdminCorrect = aclManager.isPoolAdmin(roles.poolAdmin); + assertTrue(isPoolAdminCorrect, 'roles.PoolAdmin must be pool admin'); + } + { + bool isEmergencyAdminCorrect = aclManager.isEmergencyAdmin(roles.emergencyAdmin); + assertTrue(isEmergencyAdminCorrect, 'roles.emergencyAdmin must be emergency admin'); + } + { + bool isDeployerDefaultAdmin = aclManager.hasRole(emptyBytes, deployer); + assertFalse(isDeployerDefaultAdmin, 'Deployer should not be default admin'); + } + { + bool isDeployerPoolAdmin = aclManager.isPoolAdmin(deployer); + assertFalse(isDeployerPoolAdmin, 'deployer should not be pool admin'); + } + { + bool isDeployerEmergencyAdmin = aclManager.isEmergencyAdmin(deployer); + assertFalse(isDeployerEmergencyAdmin, 'Deployer should not be emergency admin'); + } + { + bool isDeployerAssetListAdmin = aclManager.isAssetListingAdmin(deployer); + assertFalse(isDeployerAssetListAdmin, 'Deployer should not be listing admin'); + } + { + address paraswapSwapAdapterOwner = Ownable(report.paraSwapLiquiditySwapAdapter).owner(); + address paraswapRepayAdapterOwner = Ownable(report.paraSwapRepayAdapter).owner(); + address paraswapWithdrawSwapOwner = Ownable(report.paraSwapWithdrawSwapAdapter).owner(); + assertEq( + paraswapRepayAdapterOwner, + roles.poolAdmin, + 'roles.poolAdmin must be paraswap repay owner' + ); + assertEq( + paraswapSwapAdapterOwner, + roles.poolAdmin, + 'roles.poolAdmin must be paraswap liquidity swap owner' + ); + assertEq( + paraswapWithdrawSwapOwner, + roles.poolAdmin, + 'roles.poolAdmin must be paraswap withdraw swap owner' + ); + } + { + address wethGatewayOwner = Ownable(report.wrappedTokenGateway).owner(); + assertEq( + wethGatewayOwner, + roles.poolAdmin, + 'roles.poolAdmin must be WrappedTokenGateway owner' + ); + } + { + address rewardsControllerAdmin = RewardsController(report.rewardsControllerProxy) + .EMISSION_MANAGER(); + assertEq( + rewardsControllerAdmin, + report.emissionManager, + 'RewardsController Proxy EMISSION_MANAGER() does not match with deployed report.emissionManager' + ); + } + { + address emissionManagerOwner = Ownable(report.emissionManager).owner(); + assertEq( + emissionManagerOwner, + roles.poolAdmin, + 'EmissionManager owner does not match with roles.poolAdmin' + ); + } + { + address treasuryAdmin = address(uint160(uint256(vm.load(report.treasury, ADMIN_SLOT)))); + assertEq( + treasuryAdmin, + report.proxyAdmin, + 'Treasury proxy admin does not match with report.proxyAdmin' + ); + } + { + address proxyAdminOwner = Ownable(report.proxyAdmin).owner(); + assertEq( + proxyAdminOwner, + roles.poolAdmin, + 'ProxyAdmin owner does not match with roles.poolAdmin' + ); + } + } + + function testCheckPermissionsTreasuryPartner() public { + bytes32 emptyBytes; + address marketOwner = makeAddr('MARKET_OWNER'); + address emergencyAdmin = makeAddr('EMERGENCY_ADMIN'); + address poolAdmin = makeAddr('POOL_ADMIN'); + address treasuryPartner = makeAddr('TREASURY_PARTNER'); + address deployer = msg.sender; + ( + Roles memory roles, + MarketConfig memory config, + DeployFlags memory flags, + MarketReport memory deployedContracts + ) = _getMarketInput(marketOwner); + + roles.emergencyAdmin = emergencyAdmin; + roles.poolAdmin = poolAdmin; + + config.paraswapAugustusRegistry = address(new AugustusRegistryMock()); + config.paraswapFeeClaimer = address(new MockParaSwapFeeClaimer()); + config.wrappedNativeToken = address(new WETH9()); + config.treasuryPartner = treasuryPartner; + config.treasurySplitPercent = 5000; + + MarketReport memory report = deployAaveV3Testnet( + deployer, + roles, + config, + flags, + deployedContracts + ); + ACLManager aclManager = ACLManager( IPoolAddressesProvider(report.poolAddressesProvider).getACLManager() ); @@ -167,5 +317,19 @@ contract AaveV3PermissionsTest is BatchTestProcedures { 'ProxyAdmin owner does not match with roles.poolAdmin' ); } + { + address revenueSplitterPartnerA = IRevenueSplitter(report.revenueSplitter).RECIPIENT_A(); + address revenueSplitterPartnerB = IRevenueSplitter(report.revenueSplitter).RECIPIENT_B(); + assertEq( + revenueSplitterPartnerA, + report.treasury, + 'RevenueSplitter recipient A does not match report.treasury' + ); + assertEq( + revenueSplitterPartnerB, + config.treasuryPartner, + 'RevenueSplitter recipient B does not match report.treasuryPartner' + ); + } } } diff --git a/tests/DeploymentsGasLimits.t.sol b/tests/DeploymentsGasLimits.t.sol index b7411c4d..0b67a340 100644 --- a/tests/DeploymentsGasLimits.t.sol +++ b/tests/DeploymentsGasLimits.t.sol @@ -65,7 +65,10 @@ contract DeploymentsGasLimits is BatchTestProcedures { address(0), 0.0005e4, 0.0004e4, - address(0) + address(0), + address(0), + address(0), + 0 ); flags = DeployFlags(true); @@ -194,6 +197,17 @@ contract DeploymentsGasLimits is BatchTestProcedures { ); } + function test12PeripheralsTreasuryPartner() public { + config.treasuryPartner = address(1); + config.treasurySplitPercent = 5000; + new AaveV3PeripheryBatch( + roles.poolAdmin, + config, + marketReportOne.poolAddressesProvider, + address(aaveV3SetupOne) + ); + } + function testCheckInitCodeSizeBatchs() public view { uint16 maxInitCodeSize = 49152; @@ -208,6 +222,10 @@ contract DeploymentsGasLimits is BatchTestProcedures { console.log('AaveV3TokensBatch', type(AaveV3TokensBatch).creationCode.length); console.log('AaveV3HelpersBatchOne', type(AaveV3HelpersBatchOne).creationCode.length); console.log('AaveV3HelpersBatchTwo', type(AaveV3HelpersBatchTwo).creationCode.length); + console.log( + 'AaveV3PeripheryBatchTreasuryPartner', + type(AaveV3PeripheryBatch).creationCode.length + ); assertLe( type(AaveV3SetupBatch).creationCode.length, diff --git a/tests/periphery/RevenueSplitter.t.sol b/tests/periphery/RevenueSplitter.t.sol new file mode 100644 index 00000000..dee06a36 --- /dev/null +++ b/tests/periphery/RevenueSplitter.t.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PercentageMath} from 'aave-v3-core/contracts/protocol/libraries/math/PercentageMath.sol'; +import {RevenueSplitter} from 'aave-v3-periphery/contracts/treasury/RevenueSplitter.sol'; +import {IRevenueSplitterErrors} from 'aave-v3-periphery/contracts/treasury/IRevenueSplitter.sol'; +import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import 'forge-std/Test.sol'; +import 'forge-std/console2.sol'; +import 'forge-std/StdUtils.sol'; + +/// @dev Simple mock of contract without fallback function +contract WalletMock {} + +contract RevenueSplitterTest is StdUtils, Test { + using PercentageMath for uint256; + + uint256 internal constant HALF_PERCENTAGE_FACTOR = 0.5e4; + + RevenueSplitter revenueSplitter; + + address recipientA; + address recipientB; + + // add two mock tokens + IERC20 tokenA; + IERC20 tokenB; + + function setUp() public { + recipientA = makeAddr('ALICE'); + recipientB = makeAddr('BOB'); + + // set mock tokens + tokenA = IERC20(address(deployMockERC20('Token A', 'TK_A', 18))); + tokenB = IERC20(address(deployMockERC20('Token B', 'TK_B', 6))); + + revenueSplitter = new RevenueSplitter(recipientA, recipientB, 2000); + } + + function test_constructor() public view { + assertEq(revenueSplitter.RECIPIENT_A(), recipientA); + assertEq(revenueSplitter.RECIPIENT_B(), recipientB); + assertEq(revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A(), 2000); + } + + function test_constructor_revert_invalid_split_percentage() public { + vm.expectRevert(IRevenueSplitterErrors.InvalidPercentSplit.selector); + new RevenueSplitter(recipientA, recipientB, 0); + + vm.expectRevert(IRevenueSplitterErrors.InvalidPercentSplit.selector); + new RevenueSplitter(recipientA, recipientB, 100_01); + + vm.expectRevert(IRevenueSplitterErrors.InvalidPercentSplit.selector); + new RevenueSplitter(recipientA, recipientB, 100_00); + } + + function test_constructor_fuzzing(uint16 a) public { + vm.assume(a > 0 && a < 100_00); + RevenueSplitter revSplitter = new RevenueSplitter(recipientA, recipientB, a); + + assertEq(revSplitter.RECIPIENT_A(), recipientA); + assertEq(revSplitter.RECIPIENT_B(), recipientB); + assertEq(revSplitter.SPLIT_PERCENTAGE_RECIPIENT_A(), a); + } + + function test_splitFunds_fuzz_max(uint256 amountA, uint256 amountB) public { + vm.assume( + amountA <= + (type(uint256).max - HALF_PERCENTAGE_FACTOR) / + revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A() + ); + vm.assume( + amountB <= + (type(uint256).max - HALF_PERCENTAGE_FACTOR) / + revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A() + ); + _splitFunds_action(amountA, amountB); + } + + function test_splitNativeFunds_fuzz_max(uint256 amountA) public { + vm.assume( + amountA <= + (type(uint256).max - HALF_PERCENTAGE_FACTOR) / + revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A() + ); + _splitNativeFunds_action(amountA); + } + + function test_splitFunds_fuzz_realistic(uint256 amountA, uint256 amountB) public { + vm.assume(amountA < 100_000_000_000_000e18); + vm.assume(amountB < 100_000_000_000_000e18); + + _splitFunds_action(amountA, amountB); + } + + function test_splitFunds_fixed() public { + _splitFunds_action(130_321_100e18, 204_0233_000e6); + } + + function _splitFunds_action(uint256 amountA, uint256 amountB) internal { + deal(address(tokenA), address(revenueSplitter), amountA); + deal(address(tokenB), address(revenueSplitter), amountB); + + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = tokenA; + tokens[1] = tokenB; + + uint256 recipientABalanceA = amountA.percentMul(revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A()); + uint256 recipientABalanceB = amountB.percentMul(revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A()); + + uint256 recipientBBalanceA = amountA - recipientABalanceA; + uint256 recipientBBalanceB = amountB - recipientABalanceB; + + revenueSplitter.splitRevenue(tokens); + + assertEq(tokenA.balanceOf(recipientA), recipientABalanceA, 'Token A balance of recipient A'); + assertEq(tokenA.balanceOf(recipientB), recipientBBalanceA, 'Token A balance of recipient B'); + assertEq(tokenB.balanceOf(recipientA), recipientABalanceB, 'Token B balance of recipient A'); + assertEq(tokenB.balanceOf(recipientB), recipientBBalanceB, 'Token B balance of recipient B'); + } + + function _splitNativeFunds_action(uint256 amountA) internal { + address sender = makeAddr('SENDER'); + deal(sender, amountA); + + vm.prank(sender); + payable(revenueSplitter).transfer(amountA); + + uint256 recipientABalanceA = amountA.percentMul(revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A()); + + uint256 recipientBBalanceA = amountA - recipientABalanceA; + + assertEq( + address(revenueSplitter).balance, + amountA, + 'Splitter balance should be amount received by fallback' + ); + assertEq(recipientA.balance, 0, 'ETH balance of recipient A'); + assertEq(recipientB.balance, 0, 'ETH balance of recipient B'); + + revenueSplitter.splitNativeRevenue(); + + assertEq(recipientA.balance, recipientABalanceA, 'ETH balance of recipient A'); + assertEq(recipientB.balance, recipientBBalanceA, 'ETH balance of recipient B'); + assertEq(address(revenueSplitter).balance, 0, 'Splitter balance should be zero'); + } + + function test_splitFund_zeroAmount_noOp() public { + _splitFunds_action(0, 0); + } + + function test_splitNativeFund_zeroAmount_noOp() public { + _splitNativeFunds_action(0); + } + + function test_splitFund_zeroTokens_noOp() public { + IERC20[] memory emptyTokensList = new IERC20[](0); + + uint256 amountA = 10e18; + uint256 amountB = 10e8; + + deal(address(tokenA), address(revenueSplitter), amountA); + deal(address(tokenB), address(revenueSplitter), amountB); + + revenueSplitter.splitRevenue(emptyTokensList); + + assertEq(tokenA.balanceOf(recipientA), 0, 'Token A balance of recipient A'); + assertEq(tokenA.balanceOf(recipientB), 0, 'Token A balance of recipient B'); + assertEq(tokenB.balanceOf(recipientA), 0, 'Token B balance of recipient A'); + assertEq(tokenB.balanceOf(recipientB), 0, 'Token B balance of recipient B'); + assertEq(tokenA.balanceOf(address(revenueSplitter)), amountA, 'Splitter balance token A'); + assertEq(tokenB.balanceOf(address(revenueSplitter)), amountB, 'Splitter balance token B'); + } + + function test_splitFund_zeroFunds_noOp() public { + IERC20[] memory tokenList = new IERC20[](2); + tokenList[0] = tokenA; + tokenList[1] = tokenB; + + revenueSplitter.splitRevenue(tokenList); + + assertEq(tokenA.balanceOf(recipientA), 0, 'Token A balance of recipient A'); + assertEq(tokenA.balanceOf(recipientB), 0, 'Token A balance of recipient B'); + assertEq(tokenB.balanceOf(recipientA), 0, 'Token B balance of recipient A'); + assertEq(tokenB.balanceOf(recipientB), 0, 'Token B balance of recipient B'); + assertEq(tokenA.balanceOf(address(revenueSplitter)), 0, 'Splitter balance token A'); + assertEq(tokenB.balanceOf(address(revenueSplitter)), 0, 'Splitter balance token B'); + } + + function test_splitFund_reverts_randomAddress() public { + IERC20[] memory tokenList = new IERC20[](1); + + vm.expectRevert(); + revenueSplitter.splitRevenue(tokenList); + + assertEq(tokenA.balanceOf(recipientA), 0, 'Token A balance of recipient A'); + assertEq(tokenA.balanceOf(recipientB), 0, 'Token A balance of recipient B'); + assertEq(tokenB.balanceOf(recipientA), 0, 'Token B balance of recipient A'); + assertEq(tokenB.balanceOf(recipientB), 0, 'Token B balance of recipient B'); + assertEq(tokenA.balanceOf(address(revenueSplitter)), 0, 'Splitter balance token A'); + assertEq(tokenB.balanceOf(address(revenueSplitter)), 0, 'Splitter balance token B'); + } + + function test_splitNativeFund_fixedAmount() public { + _splitNativeFunds_action(11 ether); + } + + function test_splitFund_oneToken() public { + uint256 amountA = 10e18; + uint256 amountB = 10e8; + + IERC20[] memory tokenList = new IERC20[](1); + tokenList[0] = tokenA; + + deal(address(tokenA), address(revenueSplitter), amountA); + deal(address(tokenB), address(revenueSplitter), amountB); + + revenueSplitter.splitRevenue(tokenList); + + uint256 recipientABalanceA = amountA.percentMul(revenueSplitter.SPLIT_PERCENTAGE_RECIPIENT_A()); + uint256 recipientBBalanceA = amountA - recipientABalanceA; + + assertEq(tokenA.balanceOf(recipientA), recipientABalanceA, 'Token A balance of recipient A'); + assertEq(tokenA.balanceOf(recipientB), recipientBBalanceA, 'Token A balance of recipient B'); + assertEq(tokenB.balanceOf(recipientA), 0, 'Token B balance of recipient A'); + assertEq(tokenB.balanceOf(recipientB), 0, 'Token B balance of recipient B'); + assertEq(tokenA.balanceOf(address(revenueSplitter)), 0, 'Splitter balance token A'); + assertEq(tokenB.balanceOf(address(revenueSplitter)), amountB, 'Splitter balance token B'); + } + + /// @dev Test that the contract does not revert if one of the recipients does not accept native currency, for preventing one recipient from blocking the other or for rescuing in case of an account does not support receiving native currency. + function test_splitNativeFund_walletNotAcceptingFunds() public { + uint256 amountA = 10 ether; + address recipientC = address(new WalletMock()); + RevenueSplitter revenueSplitterInstance = new RevenueSplitter(recipientA, recipientC, 2000); + + deal(address(revenueSplitterInstance), amountA); + + uint256 recipientABalanceA = amountA.percentMul( + revenueSplitterInstance.SPLIT_PERCENTAGE_RECIPIENT_A() + ); + uint256 remaining = amountA - recipientABalanceA; + + assertEq( + address(revenueSplitterInstance).balance, + amountA, + 'Splitter balance should be equal to amountA' + ); + assertEq(recipientA.balance, 0, 'ETH balance of recipient A'); + assertEq(recipientC.balance, 0, 'ETH balance of recipient C'); + + revenueSplitterInstance.splitNativeRevenue(); + + assertEq(recipientA.balance, recipientABalanceA, 'ETH balance of recipient A'); + assertEq( + recipientC.balance, + 0, + 'ETH balance of recipient C should be zero due it does not contain fallback function' + ); + assertEq( + address(revenueSplitterInstance).balance, + remaining, + 'Splitter balance should be the remaining' + ); + } +} diff --git a/tests/utils/BatchTestProcedures.sol b/tests/utils/BatchTestProcedures.sol index 3f69a9c0..eb1f7d07 100644 --- a/tests/utils/BatchTestProcedures.sol +++ b/tests/utils/BatchTestProcedures.sol @@ -189,9 +189,14 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput assertTrue(r.aaveOracle != address(0), 'report.aaveOracle'); assertTrue(r.defaultInterestRateStrategy != address(0), 'report.defaultInterestRateStrategy'); assertTrue(r.aclManager != address(0), 'report.aclManager'); - assertTrue(r.treasury != address(0), 'report.treasury'); assertTrue(r.proxyAdmin != address(0), 'report.proxyAdmin'); - assertTrue(r.treasuryImplementation != address(0), 'report.treasuryImplementation'); + if (config.treasury == address(0)) { + assertTrue(r.treasury != address(0), 'report.treasury'); + assertTrue(r.treasuryImplementation != address(0), 'report.treasuryImplementation'); + } else { + assertTrue(r.treasury == config.treasury, 'report.treasury'); + assertTrue(r.treasuryImplementation == address(0), 'report.treasuryImplementation'); + } assertTrue(r.wrappedTokenGateway != address(0), 'report.wrappedTokenGateway'); assertTrue(r.walletBalanceProvider != address(0), 'report.walletBalanceProvider'); assertTrue(r.uiIncentiveDataProvider != address(0), 'report.uiIncentiveDataProvider'); @@ -242,6 +247,10 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput assertTrue(r.staticATokenFactoryProxy != address(0), 'report.staticATokenFactoryProxy'); assertTrue(r.staticATokenImplementation != address(0), 'report.staticATokenImplementation'); assertTrue(r.transparentProxyFactory != address(0), 'report.transparentProxyFactory'); + + if (config.treasuryPartner != address(0)) { + assertTrue(r.revenueSplitter != address(0), 'report.revenueSplitter'); + } } function deployAaveV3Testnet(