modifier validSplit(
address[] memory accounts,
uint32[] memory percentAllocations,
uint32 distributorFee
) {
if (accounts.length < 2)
revert InvalidSplit__TooFewAccounts(accounts.length);
if (accounts.length != percentAllocations.length)
revert InvalidSplit__AccountsAndAllocationsMismatch(
accounts.length,
percentAllocations.length
);
// _getSum should overflow if any percentAllocation[i] < 0
if (_getSum(percentAllocations) != PERCENTAGE_SCALE)
revert InvalidSplit__InvalidAllocationsSum(_getSum(percentAllocations));
unchecked {
// overflow should be impossible in for-loop index
// cache accounts length to save gas
uint256 loopLength = accounts.length - 1;
for (uint256 i = 0; i < loopLength; ++i) {
// overflow should be impossible in array access math
-> if (accounts[i] >= accounts[i + 1])
revert InvalidSplit__AccountsOutOfOrder(i);
if (percentAllocations[i] == uint32(0))
revert InvalidSplit__AllocationMustBePositive(i);
}
// overflow should be impossible in array access math with validated equal array lengths
if (percentAllocations[loopLength] == uint32(0))
revert InvalidSplit__AllocationMustBePositive(loopLength);
}
if (distributorFee > MAX_DISTRIBUTOR_FEE)
revert InvalidSplit__InvalidDistributorFee(distributorFee);
_;
}
This means that the TitlesCore.createEdition() function and the FeeManager.createRoute() function will end up reverting with the error: InvalidSplit__AccountsOutOfOrder() if the accounts are not ordered.
The accounts passed to the splitFactory.createSplit() function get built inside the FeeManager._buildSharesAndTargets() function:
As we can see, the first target will always be set to creator.target:
targets[0] = creator.target;
This means that, splitFactory.createSplit() will always fail if target[1..n] < creator.target, meaning people will not be able to TitlesCore.createEdition() depending on the attributions supplied and it will not be possible to FeeManager.createRoute() depending on the attributions supplied.
This is especially a problem because the first address of the target[] is always set to creator.target , which means it can not be swapped another index to keep the array in order.
Proof of Concept
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
interface ISplitFactory {
function createSplit(
address[] calldata accounts,
uint32[] calldata percentAllocations,
uint32 distributorFee,
address controller
)
external
returns (address split);
}
contract TitlesE2E is Test {
// https://etherscan.io/address/0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE#code
address split_factory_address = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE;
ISplitFactory splitFactory = ISplitFactory(split_factory_address);
function setUp() public {
// Create the fork
// Input your RPC here
vm.createSelectFork("inputRPCHere");
}
function test_poc() public {
// Sanity check
// We took a legit transaction that was made on the ETH mainnet - this will pass
// https://etherscan.io/tx/0x3ed250d745f21cd8d2ee454fe4a97eb661cda62216bd33bd056c79242e6eaec3
address(splitFactory).call(hex"7601f782000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c01b3424b81da02a3ab626a44cfe67c2eb15d9da0000000000000000000000000000000000000000000000000000000000000011000000000000000000000000077aac78a05f5a116fde61cd4ef3081c175315c2000000000000000000000000133890338e33141727b2252cc92f3081ca2120800000000000000000000000001bcaff43a8607b0416d9e71586498a16c3486f0300000000000000000000000029d489e1c200740d91fce85614a0a13376eda87a00000000000000000000000046761f1d443531afc94080e9f93d7aad7c75ae87000000000000000000000000644e6abb3f9e8754bd9e07159f4ee7d5e3c5c4ac0000000000000000000000006d62d8e11b342d4d1f3054dd4c9d9540fea76b3a00000000000000000000000087c926b07162be81ed2af05e3307a7d4a3f0ade10000000000000000000000008ed3e58fcf1f2169f7b39fc1aa8d753719ec53a800000000000000000000000094d94ef8422418fc5e8aba0db5900a3454d8029e000000000000000000000000a05a94773154de8c0eed5fdf7d1889efea02dc9a000000000000000000000000a7ecbb281fb3fe21943928282cb4452279b4a65c000000000000000000000000b5c1a33bbc95ce8541af2e9c407f3989f9dabdd0000000000000000000000000b74f97da97609ed9a592a12a4641ebc5cd602406000000000000000000000000c01b3424b81da02a3ab626a44cfe67c2eb15d9da000000000000000000000000db9ae004be5b96125685756c75b177189d5b47a6000000000000000000000000e3525546721893aa484dda2b7139ba71d751be5e000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000053020000000000000000000000000000000000000000000000000000000000019f0a0000000000000000000000000000000000000000000000000000000000005302000000000000000000000000000000000000000000000000000000000000a6040000000000000000000000000000000000000000000000000000000000005302000000000000000000000000000000000000000000000000000000000001afa4000000000000000000000000000000000000000000000000000000000000530200000000000000000000000000000000000000000000000000000000000053020000000000000000000000000000000000000000000000000000000000005302000000000000000000000000000000000000000000000000000000000000a604000000000000000000000000000000000000000000000000000000000000530200000000000000000000000000000000000000000000000000000000000056ea000000000000000000000000000000000000000000000000000000000004cadb000000000000000000000000000000000000000000000000000000000000a604000000000000000000000000000000000000000000000000000000000001f4af00000000000000000000000000000000000000000000000000000000000053020000000000000000000000000000000000000000000000000000000000005302");
// However, if we change the first address from:
// 0x077AAC78a05F5a116Fde61Cd4eF3081c175315c2
// To:
// 0x177AAC78a05F5a116Fde61Cd4eF3081c175315c2
// It will revert because:
// 0x177AAC78a05F5a116Fde61Cd4eF3081c175315c2 > 0x133890338E33141727B2252Cc92F3081Ca212080
vm.expectRevert();
address(splitFactory).call(hex"7601f782000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c01b3424b81da02a3ab626a44cfe67c2eb15d9da0000000000000000000000000000000000000000000000000000000000000011000000000000000000000000177aac78a05f5a116fde61cd4ef3081c175315c2000000000000000000000000133890338e33141727b2252cc92f3081ca2120800000000000000000000000001bcaff43a8607b0416d9e71586498a16c3486f0300000000000000000000000029d489e1c200740d91fce85614a0a13376eda87a00000000000000000000000046761f1d443531afc94080e9f93d7aad7c75ae87000000000000000000000000644e6abb3f9e8754bd9e07159f4ee7d5e3c5c4ac0000000000000000000000006d62d8e11b342d4d1f3054dd4c9d9540fea76b3a00000000000000000000000087c926b07162be81ed2af05e3307a7d4a3f0ade10000000000000000000000008ed3e58fcf1f2169f7b39fc1aa8d753719ec53a800000000000000000000000094d94ef8422418fc5e8aba0db5900a3454d8029e000000000000000000000000a05a94773154de8c0eed5fdf7d1889efea02dc9a000000000000000000000000a7ecbb281fb3fe21943928282cb4452279b4a65c000000000000000000000000b5c1a33bbc95ce8541af2e9c407f3989f9dabdd0000000000000000000000000b74f97da97609ed9a592a12a4641ebc5cd602406000000000000000000000000c01b3424b81da02a3ab626a44cfe67c2eb15d9da000000000000000000000000db9ae004be5b96125685756c75b177189d5b47a6000000000000000000000000e3525546721893aa484dda2b7139ba71d751be5e000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000053020000000000000000000000000000000000000000000000000000000000019f0a0000000000000000000000000000000000000000000000000000000000005302000000000000000000000000000000000000000000000000000000000000a6040000000000000000000000000000000000000000000000000000000000005302000000000000000000000000000000000000000000000000000000000001afa4000000000000000000000000000000000000000000000000000000000000530200000000000000000000000000000000000000000000000000000000000053020000000000000000000000000000000000000000000000000000000000005302000000000000000000000000000000000000000000000000000000000000a604000000000000000000000000000000000000000000000000000000000000530200000000000000000000000000000000000000000000000000000000000056ea000000000000000000000000000000000000000000000000000000000004cadb000000000000000000000000000000000000000000000000000000000000a604000000000000000000000000000000000000000000000000000000000001f4af00000000000000000000000000000000000000000000000000000000000053020000000000000000000000000000000000000000000000000000000000005302");
}
}
zoyi
high
Creating editions can fail and creating routes can fail due to un-ordering of targets
Summary
Due to an un-ordered array,
TitlesCore.createEdition
andFeeManager.createRoute()
can fail.Vulnerability Detail
During the
TitlesCore.createEdition()
execution flow, insideFeeManager.createRoute()
,splitFactory.createSplit()
is called:If we take a look at
splitFactory.createSplit()
, we'll see that the passed accounts are required to be ordered. See this comment: SplitMain.sol#L250This check happens in the modifier
validSplit()
: SplitMain.sol#L184-L217This means that the
TitlesCore.createEdition()
function and theFeeManager.createRoute()
function will end up reverting with the error:InvalidSplit__AccountsOutOfOrder()
if the accounts are not ordered.The accounts passed to the
splitFactory.createSplit()
function get built inside theFeeManager._buildSharesAndTargets()
function:As we can see, the first target will always be set to
creator.target
:This means that,
splitFactory.createSplit()
will always fail iftarget[1..n] < creator.target
, meaning people will not be able toTitlesCore.createEdition()
depending on theattributions
supplied and it will not be possible toFeeManager.createRoute()
depending on theattributions
supplied.This is especially a problem because the first address of the
target[]
is always set tocreator.target
, which means it can not be swapped another index to keep the array in order.Proof of Concept
Running
test_poc
:First one succeeds - second one reverts because it's not ordered.
Impact
Editions and Routes will not be able to be created, breaking the core functionality of this project.
Code Snippet
FeeManager.sol#L144
Tool used
Manual Review
Recommendation
Order the addresses before passing them to
createSplit()
.