Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/periphery/FeeSplitter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
pragma solidity ^0.8.18;

import "lyra-utils/decimals/SignedDecimalMath.sol";

import {Ownable2Step} from "openzeppelin/access/Ownable2Step.sol";
import {IManager} from "v2-core/src/interfaces/IManager.sol";
import {ISubAccounts, IAsset} from "v2-core/src/interfaces/ISubAccounts.sol";

/**
* @title FeeSplitter
* @notice FeeSplitter is a contract that splits the balance of a subaccount held by this contract based on a % split
* @author Lyra
*/
contract FeeSplitter is Ownable2Step {
using SignedDecimalMath for int;

ISubAccounts public immutable subAccounts;
IAsset public immutable cashAsset;

uint public subAcc;
uint public splitPercent;
uint public accountA;
uint public accountB;

constructor(
ISubAccounts _subAccounts,
IManager manager,
IAsset _cashAsset,
uint _splitPercent,
uint _accountA,
uint _accountB
) {
subAccounts = _subAccounts;
subAcc = _subAccounts.createAccount(address(this), manager);
cashAsset = _cashAsset;

_setSplit(_splitPercent);
_setSubAccounts(_accountA, _accountB);
}

///////////
// Admin //
///////////

/// @notice Set the % split
function setSplit(uint _splitPercent) external onlyOwner {
_setSplit(_splitPercent);
}

function _setSplit(uint _splitPercent) internal {
if (_splitPercent > 1e18) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think cash and managers allow amount=0 transfers, but might be worth double checking.

E.g. if _splitPercent is 0 or 1, you'd need to make 0 amount transfers in split()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed test for this 👌

revert FS_InvalidSplitPercentage();
}
splitPercent = _splitPercent;

emit SplitPercentSet(_splitPercent);
}

/// @notice Set the subaccounts to split the balance between
function setSubAccounts(uint _accountA, uint _accountB) external onlyOwner {
_setSubAccounts(_accountA, _accountB);
}

function _setSubAccounts(uint _accountA, uint _accountB) internal {
if (_accountA == 0 || _accountB == 0) {
revert FS_InvalidSubAccount();
}

accountA = _accountA;
accountB = _accountB;

emit SubAccountsSet(_accountA, _accountB);
}

/// @notice Recover a subaccount held by this contract, creating a new one in its place
function recoverSubAccount(address recipient) external onlyOwner {
uint oldSubAcc = subAcc;
subAccounts.transferFrom(address(this), recipient, oldSubAcc);
subAcc = subAccounts.createAccount(address(this), subAccounts.manager(oldSubAcc));

emit SubAccountRecovered(oldSubAcc, recipient, subAcc);
}

//////////////
// External //
//////////////
/// @notice Work out the balance of the subaccount held by this contract, and split it based on the % split
function split() external {
int balance = subAccounts.getBalance(subAcc, cashAsset, 0);

if (balance <= 0) {
revert FS_NoBalanceToSplit();
}

int splitAmountA = balance.multiplyDecimal(int(splitPercent));
int splitAmountB = balance - splitAmountA;

ISubAccounts.AssetTransfer[] memory transfers = new ISubAccounts.AssetTransfer[](2);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a check that accountA and accountB are NOT 0. Otherwise I think you might accidentally send fees to the subaccountId 0 lol.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or would run setSubaccounts in constructor.

transfers[0] = ISubAccounts.AssetTransfer({
fromAcc: subAcc,
toAcc: accountA,
asset: cashAsset,
subId: 0,
amount: int(splitAmountA),
assetData: bytes32(0)
});
transfers[1] = ISubAccounts.AssetTransfer({
fromAcc: subAcc,
toAcc: accountB,
asset: cashAsset,
subId: 0,
amount: int(splitAmountB),
assetData: bytes32(0)
});

subAccounts.submitTransfers(transfers, "");

emit BalanceSplit(subAcc, accountA, accountB, splitAmountA, splitAmountB);
}

////////////
// Errors //
////////////
error FS_InvalidSplitPercentage();
error FS_InvalidSubAccount();
error FS_NoBalanceToSplit();

////////////
// Events //
////////////
event SplitPercentSet(uint splitPercent);
event SubAccountsSet(uint accountA, uint accountB);
event SubAccountRecovered(uint oldSubAcc, address recipient, uint newSubAcc);
event BalanceSplit(uint subAcc, uint accountA, uint accountB, int splitAmountA, int splitAmountB);
}
109 changes: 109 additions & 0 deletions test/periphery/FeeSplitter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
pragma solidity ^0.8.18;

import {IntegrationTestBase} from "v2-core/test/integration-tests/shared/IntegrationTestBase.t.sol";
import {IManager} from "v2-core/src/interfaces/IManager.sol";
import {IAsset} from "v2-core/src/interfaces/IAsset.sol";
import {ISubAccounts} from "v2-core/src/interfaces/ISubAccounts.sol";

import "forge-std/console2.sol";
import "../../src/periphery/FeeSplitter.sol";

contract FeeSplitterTest is IntegrationTestBase {
FeeSplitter public feeSplitter;
IAsset public asset;
uint public accountA;
uint public accountB;

function setUp() public {
_setupIntegrationTestComplete();

accountA = subAccounts.createAccount(address(this), srm);
accountB = subAccounts.createAccount(address(this), markets["weth"].pmrm);

feeSplitter = new FeeSplitter(subAccounts, srm, cash, 0.5e18, accountA, accountB);
uint amount = 100e6;
usdc.mint(address(this), amount);
usdc.approve(address(cash), amount);
cash.deposit(feeSplitter.subAcc(), amount);
}

function test_constructor() public {
assertEq(address(feeSplitter.subAccounts()), address(subAccounts), "Incorrect subAccounts");
assertEq(address(feeSplitter.cashAsset()), address(cash), "Incorrect cashAsset");
assertFalse(feeSplitter.subAcc() == 0, "Incorrect subAcc");
assertEq(feeSplitter.splitPercent(), 0.5e18, "Incorrect splitPercent");
assertEq(feeSplitter.accountA(), accountA, "Incorrect accountA");
assertEq(feeSplitter.accountB(), accountB, "Incorrect accountB");
}

function test_setSplit() public {
feeSplitter.setSplit(0.6e18);
assertEq(feeSplitter.splitPercent(), 0.6e18, "Incorrect splitPercent");

vm.expectRevert(FeeSplitter.FS_InvalidSplitPercentage.selector);
feeSplitter.setSplit(1.1e18);
}

function test_split() public {
// before split
assertEq(
subAccounts.getBalance(feeSplitter.subAcc(), cash, 0), int(100e18), "Incorrect initial balance in FeeSplitter"
);
assertEq(subAccounts.getBalance(accountA, cash, 0), 0, "Account A should initially have 0 balance");
assertEq(subAccounts.getBalance(accountB, cash, 0), 0, "Account B should initially have 0 balance");

feeSplitter.split();

// after split
assertEq(
subAccounts.getBalance(accountA, cash, 0), int(50e18), "Account A should have half of the funds after split"
);
assertEq(
subAccounts.getBalance(accountB, cash, 0), int(50e18), "Account B should have half of the funds after split"
);

vm.expectRevert(FeeSplitter.FS_NoBalanceToSplit.selector);
feeSplitter.split();
}

function test_recover() public {
uint oldSubAcc = feeSplitter.subAcc();
feeSplitter.recoverSubAccount(address(this));
uint newSubAcc = feeSplitter.subAcc();
assertEq(
subAccounts.ownerOf(newSubAcc), address(feeSplitter), "New subAcc should be owned by fee splitter contract"
);
assertEq(subAccounts.ownerOf(oldSubAcc), address(this), "Old subAcc should be owned by this contract");
assertFalse(oldSubAcc == newSubAcc);
}

function test_split100percent() public {
feeSplitter.setSplit(1e18);
feeSplitter.split();
assertEq(
subAccounts.getBalance(accountA, cash, 0), int(100e18), "Account A should have all of the funds after split"
);
assertEq(subAccounts.getBalance(accountB, cash, 0), 0, "Account B should have 0 balance after split");
}

function test_split0percent() public {
feeSplitter.setSplit(0);
feeSplitter.split();
assertEq(subAccounts.getBalance(accountA, cash, 0), 0, "Account A should have 0 balance after split");
assertEq(
subAccounts.getBalance(accountB, cash, 0), int(100e18), "Account B should have all of the funds after split"
);
}

function test_setSubAccounts() public {
feeSplitter.setSubAccounts(accountB, accountA);
assertEq(feeSplitter.accountA(), accountB, "Incorrect accountA");
assertEq(feeSplitter.accountB(), accountA, "Incorrect accountB");

vm.expectRevert(FeeSplitter.FS_InvalidSubAccount.selector);
feeSplitter.setSubAccounts(0, accountA);

vm.expectRevert(FeeSplitter.FS_InvalidSubAccount.selector);
feeSplitter.setSubAccounts(accountB, 0);
}
}