Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
112 changes: 112 additions & 0 deletions src/periphery/FeeSplitter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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 = 0.5e18;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: why not just force it in the constructor. Seems like an important value to not forget to set.

uint public accountA;
uint public accountB;

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

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

/// @notice Set the % split
function setSplit(uint _splitPercent) external onlyOwner {
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 {
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_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);
}
81 changes: 81 additions & 0 deletions test/periphery/FeeSplitter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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);
feeSplitter.setSubAccounts(accountA, accountB);
feeSplitter.setSplit(0.5e18);
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);
}
}