diff --git a/packages/evm/contracts/utils/MimicHelper.sol b/packages/evm/contracts/utils/MimicHelper.sol index cc80ed9..27d1b6c 100644 --- a/packages/evm/contracts/utils/MimicHelper.sol +++ b/packages/evm/contracts/utils/MimicHelper.sol @@ -14,11 +14,16 @@ 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 */ contract MimicHelper { + // Basis points scale + uint16 internal constant BPS_SCALE = 10_000; + // Custom byte storage per user and key mapping (address => mapping (string => bytes)) internal _customStorage; @@ -27,6 +32,11 @@ contract MimicHelper { */ event StorageSet(address indexed user, string indexed key, bytes indexed data); + /** + * @dev A single percent is greater than 10_000 basis points + */ + error MimicHelperInvalidPercent(); + /** * @dev Tells the native token balance of an address * @param target Address to get native token balance @@ -61,4 +71,14 @@ contract MimicHelper { _customStorage[msg.sender][key] = data; emit StorageSet(msg.sender, key, data); } + + /** + * @dev Tells a percentage of an amount, rounding down + * @param amount Amount to compute the percentage of + * @param percent Percentage to apply, expressed in basis points. 10_000 = 100%, 50 = 0.5% + */ + function pct(uint256 amount, uint16 percent) external pure returns (uint256) { + if (percent > BPS_SCALE) revert MimicHelperInvalidPercent(); + return Math.mulDiv(amount, percent, BPS_SCALE); + } } diff --git a/packages/evm/test/utils/MimicHelper.test.ts b/packages/evm/test/utils/MimicHelper.test.ts index e25d5f1..2ca47e0 100644 --- a/packages/evm/test/utils/MimicHelper.test.ts +++ b/packages/evm/test/utils/MimicHelper.test.ts @@ -116,4 +116,48 @@ describe('MimicHelper', () => { }) }) }) + + describe('pct', () => { + const BPS_SCALE = 10_000 + + context('when the percent is between 0 and 10_000', () => { + context('when amount is not 0', () => { + const amount = 101n + + it('returns the percentage rounding down', async () => { + expect(await mimicHelper.pct(amount, 0)).to.be.equal(0n) + expect(await mimicHelper.pct(amount, 50)).to.be.equal(0n) + expect(await mimicHelper.pct(amount, 100)).to.be.equal(1n) + expect(await mimicHelper.pct(amount, 5000)).to.be.equal(50n) + expect(await mimicHelper.pct(amount, BPS_SCALE)).to.be.equal(amount) + }) + + it('always rounds down', async () => { + expect(await mimicHelper.pct(1, 9900)).to.equal(0n) + expect(await mimicHelper.pct(3, 3333)).to.equal(0n) + expect(await mimicHelper.pct(3, 3334)).to.equal(1n) + expect(await mimicHelper.pct(4, 3333)).to.equal(1n) + expect(await mimicHelper.pct(99, 3300)).to.equal(32n) + expect(await mimicHelper.pct(100, 3300)).to.equal(33n) + }) + }) + + context('when amount is 0', () => { + it('returns 0', async () => { + expect(await mimicHelper.pct(0, 0)).to.equal(0n) + expect(await mimicHelper.pct(0, 5000)).to.equal(0n) + expect(await mimicHelper.pct(0, BPS_SCALE)).to.equal(0n) + }) + }) + }) + + context('when the percent is greater than 10_000', () => { + it('reverts', async () => { + await expect(mimicHelper.pct(1, BPS_SCALE + 1)).to.be.revertedWithCustomError( + mimicHelper, + 'MimicHelperInvalidPercent' + ) + }) + }) + }) })