Skip to content

Commit

Permalink
feat: Add RevenueSplitter, add RevenueSplitter config to batch deploy…
Browse files Browse the repository at this point in the history
…ments (aave-dao#37)

* feat: add RevenueSplitter, add RevenueSplitter config to batch deployments

* feat: add native currency split, add tests

* chore: run lint

* feat: add ReentrancyGuard to RevenueSplitter

* chore: add test for splitNativeFund with smart contract wallet recipient that does NOT have fallback function

* chore: fix assert message with correct recipient

* chore: run linter

* chore: add specs to IRevenueSplitter interface

* chore: rephrase spec

* chore: move config.incentivesProxy input logic into separate PR 'feat/reuse-rewards-controller'

* chore: change license of RevenueSplitter.sol to BUSL-1.1

---------

Co-authored-by: kartojal <david@catapulta.sh>
  • Loading branch information
kartojal and kartojal committed Aug 21, 2024
1 parent f444b34 commit 6948864
Show file tree
Hide file tree
Showing 13 changed files with 675 additions and 32 deletions.
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
20 changes: 2 additions & 18 deletions src/deployments/contracts/procedures/AaveV3TreasuryProcedure.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import '../../interfaces/IMarketReportTypes.sol';
contract AaveV3TreasuryProcedure {
struct TreasuryReport {
address treasuryImplementation;
address proxyAdmin;
address treasury;
}

Expand All @@ -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),
Expand All @@ -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),
Expand Down
5 changes: 5 additions & 0 deletions src/deployments/interfaces/IMarketReportTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ struct MarketReport {
address staticATokenFactoryImplementation;
address staticATokenFactoryProxy;
address staticATokenImplementation;
address revenueSplitter;
}

struct LibrariesReport {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -177,6 +181,7 @@ struct PeripheryReport {
address treasuryImplementation;
address emissionManager;
address rewardsControllerImplementation;
address revenueSplitter;
}

struct ParaswapReport {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -328,6 +333,7 @@ library AaveV3BatchOrchestration {
report.staticATokenFactoryProxy = staticATokenReport.staticATokenFactoryProxy;
report.staticATokenImplementation = staticATokenReport.staticATokenImplementation;
report.transparentProxyFactory = staticATokenReport.transparentProxyFactory;
report.revenueSplitter = peripheryReport.revenueSplitter;

return report;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
43 changes: 43 additions & 0 deletions src/periphery/contracts/treasury/IRevenueSplitter.sol
Original file line number Diff line number Diff line change
@@ -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);
}
73 changes: 73 additions & 0 deletions src/periphery/contracts/treasury/RevenueSplitter.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}
48 changes: 47 additions & 1 deletion tests/AaveV3BatchDeployment.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -52,7 +56,10 @@ contract AaveV3BatchDeployment is BatchTestProcedures {
address(0),
0.0005e4,
0.0004e4,
address(0)
address(0),
address(0),
address(0),
0
);
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
}
5 changes: 4 additions & 1 deletion tests/AaveV3BatchTests.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading

0 comments on commit 6948864

Please sign in to comment.