diff --git a/packages/evm/contracts/utils/MimicHelper.sol b/packages/evm/contracts/utils/MimicHelper.sol index cc80ed9..f13f693 100644 --- a/packages/evm/contracts/utils/MimicHelper.sol +++ b/packages/evm/contracts/utils/MimicHelper.sol @@ -14,6 +14,8 @@ pragma solidity ^0.8.20; +import { Math } from '@openzeppelin/contracts/utils/math/Math.sol'; + /** * @title Mimic Helper * @dev Collection of helper functions for the Mimic Protocol @@ -27,6 +29,16 @@ contract MimicHelper { */ event StorageSet(address indexed user, string indexed key, bytes indexed data); + /** + * @dev The percents add up to 100 or more + */ + error MimicHelperInvalidPercent(); + + /** + * @dev The percents array is empty + */ + error MimicHelperEmptyPercents(); + /** * @dev Tells the native token balance of an address * @param target Address to get native token balance @@ -61,4 +73,30 @@ contract MimicHelper { _customStorage[msg.sender][key] = data; emit StorageSet(msg.sender, key, data); } + + /** + * @dev Splits an amount by specified percentages. + * All remainder goes to the last split. + * @param amount Amount to be split + * @param percents Array of percents + * @return splits Array of `percents.length + 1` amounts; the last element holds the remainder + */ + function pct(uint256 amount, uint8[] calldata percents) external pure returns (uint256[] memory splits) { + uint256 len = percents.length; + if (len == 0) revert MimicHelperEmptyPercents(); + + splits = new uint256[](len + 1); + uint256 pctSum = 0; + uint256 amountSum = 0; + + for (uint256 i = 0; i < len; i++) { + splits[i] = Math.mulDiv(amount, percents[i], 100); + amountSum += splits[i]; + pctSum += percents[i]; + } + if (pctSum >= 100) revert MimicHelperInvalidPercent(); + + splits[len] = amount - amountSum; // absorbs rounding dust; never underflows since pctSum < 100 + return splits; + } } diff --git a/packages/evm/test/utils/MimicHelper.test.ts b/packages/evm/test/utils/MimicHelper.test.ts index e25d5f1..4b420c5 100644 --- a/packages/evm/test/utils/MimicHelper.test.ts +++ b/packages/evm/test/utils/MimicHelper.test.ts @@ -116,4 +116,59 @@ describe('MimicHelper', () => { }) }) }) + + describe('pct', () => { + context('when the percents leave a remainder', () => { + const amount = 101n + const percents = [50] + + it('returns splits that add up to the total', async () => { + const splits = await mimicHelper.pct(amount, percents) + + expect(splits).to.have.lengthOf(percents.length + 1) + expect(splits[0]).to.be.equal(50n) + expect(splits[1]).to.be.equal(51n) + expect(splits.reduce((total, split) => total + split, 0n)).to.be.equal(amount) + }) + }) + + context('when there are multiple percents', () => { + const amount = 100n + const percents = [30, 30] + + it('returns a split per percent plus the remainder', async () => { + const splits = await mimicHelper.pct(amount, percents) + + expect(splits).to.have.lengthOf(percents.length + 1) + expect(splits[0]).to.be.equal(30n) + expect(splits[1]).to.be.equal(30n) + expect(splits[2]).to.be.equal(40n) + expect(splits.reduce((total, split) => total + split, 0n)).to.be.equal(amount) + }) + }) + + context('when the percents sum to 100% or more', () => { + const amount = 101n + const percents = [50, 50] + + it('reverts', async () => { + await expect(mimicHelper.pct(amount, percents)).to.be.revertedWithCustomError( + mimicHelper, + 'MimicHelperInvalidPercent' + ) + }) + }) + + context('when the percents array is empty', () => { + const amount = 101n + const percents: number[] = [] + + it('reverts', async () => { + await expect(mimicHelper.pct(amount, percents)).to.be.revertedWithCustomError( + mimicHelper, + 'MimicHelperEmptyPercents' + ) + }) + }) + }) })