diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 7550749..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: "CI" - -env: - API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }} - FOUNDRY_PROFILE: "ci" - -on: - workflow_dispatch: - pull_request: - push: - branches: - - "main" - -jobs: - lint: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Bun" - uses: "oven-sh/setup-bun@v1" - - - name: "Install the Node.js dependencies" - run: "bun install" - - - name: "Lint the code" - run: "bun run lint" - - - name: "Add lint summary" - run: | - echo "## Lint result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - build: - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Bun" - uses: "oven-sh/setup-bun@v1" - - - name: "Install the Node.js dependencies" - run: "bun install" - - - name: "Build the contracts and print their size" - run: "forge build --sizes" - - - name: "Add build summary" - run: | - echo "## Build result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - test: - needs: ["lint", "build"] - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Install Foundry" - uses: "foundry-rs/foundry-toolchain@v1" - - - name: "Install Bun" - uses: "oven-sh/setup-bun@v1" - - - name: "Install the Node.js dependencies" - run: "bun install" - - - name: "Show the Foundry config" - run: "forge config" - - - name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance" - run: > - echo "FOUNDRY_FUZZ_SEED=$( - echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) - )" >> $GITHUB_ENV - - - name: "Run the tests" - run: "forge test" - - - name: "Add test summary" - run: | - echo "## Tests result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/use-template.yml b/.github/workflows/use-template.yml deleted file mode 100644 index 76f314d..0000000 --- a/.github/workflows/use-template.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: "Create" - -# The workflow will run when the "Use this template" button is used -on: - push: - -jobs: - create: - # We only run this action when the repository isn't the template repository. References: - # - https://docs.github.com/en/actions/learn-github-actions/contexts - # - https://docs.github.com/en/actions/learn-github-actions/expressions - if: ${{ !github.event.repository.is_template }} - permissions: "write-all" - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Update package.json" - env: - GITHUB_REPOSITORY_DESCRIPTION: ${{ github.event.repository.description }} - run: ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" - - - name: "Add rename summary" - run: | - echo "## Commit result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - - name: "Remove files not needed in the user's copy of the template" - run: | - rm -f "./.github/FUNDING.yml" - rm -f "./.github/scripts/rename.sh" - rm -f "./.github/workflows/create.yml" - - - name: "Add remove summary" - run: | - echo "## Remove result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - - name: "Update commit" - uses: "stefanzweifel/git-auto-commit-action@v4" - with: - commit_message: "feat: initial commit" - commit_options: "--amend" - push_options: "--force" - skip_fetch: true - - - name: "Add commit summary" - run: | - echo "## Commit result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 149e93e..914c4e4 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.25 <0.9.0; -import { FeeRouterEarningsAirdrop } from "../src/FeeRouterEarningsAirdrop.sol"; +import { MemoryDistributor } from "../src/MemoryDistributor.sol"; import { BaseScript } from "./Base.s.sol"; @@ -13,8 +13,8 @@ contract Deploy is BaseScript { ) public broadcast - returns (FeeRouterEarningsAirdrop feeRouterEarningsAirdrop) + returns (MemoryDistributor memoryDistributor) { - feeRouterEarningsAirdrop = new FeeRouterEarningsAirdrop(_initialOwner, _airdropToken); + memoryDistributor = new MemoryDistributor(_initialOwner, _airdropToken); } } diff --git a/src/FeeRouterEarningsAirdrop.sol b/src/FeeRouterEarningsAirdrop.sol deleted file mode 100644 index e77c2ac..0000000 --- a/src/FeeRouterEarningsAirdrop.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.25; - -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -struct AirdropClaim { - uint256 round; - uint256 amount; - bytes32[] merkleProof; -} - -contract FeeRouterEarningsAirdrop is Ownable, ReentrancyGuard { - ////////////////////////////// Libraries ////////////////////////////// - - using SafeERC20 for IERC20; // Prevent sending tokens to recipients who can’t receive - - ////////////////////////////// State ////////////////////////////// - - IERC20 public immutable AIRDROP_TOKEN; - mapping(uint256 round => bytes32 merkleRoot) public merkleRoots; - mapping(uint256 round => mapping(address account => bool claimed)) public hasClaimed; - - ////////////////////////////// Errors ///////////////////////////// - - error InvalidMerkleRoot(); - error MerkleRootNotSet(uint256 round); - error MerkleRootAlreadySet(uint256 round); - error AlreadyClaimed(uint256 round, address account); - error InvalidProof(); - error EmptyAirdropClaim(); - - ////////////////////////////// Events ///////////////////////////// - - event Claimed(uint256 round, address account, uint256 amount); - event MerkleRootSet(uint256 round, bytes32 merkleRoot); - - ////////////////////////////// Constructor ///////////////////////////// - - constructor(address _initialOwner, address _airdropToken) Ownable(_initialOwner) { - AIRDROP_TOKEN = IERC20(_airdropToken); - } - - ////////////////////////////// External Methods ////////////////////////////// - - function claim(AirdropClaim[] calldata airdropClaims) external nonReentrant { - // Revert if the airdrop claims are empty - if (airdropClaims.length == 0) { - revert EmptyAirdropClaim(); - } - - for (uint256 i = 0; i < airdropClaims.length; i++) { - _claim(airdropClaims[i]); - } - } - - function setRound(uint256 round, bytes32 merkleRoot) external onlyOwner { - // Merkle root must not be empty - if (merkleRoot == bytes32(0)) { - revert InvalidMerkleRoot(); - } - - // Round must not be set yet - if (merkleRoots[round] != bytes32(0)) { - revert MerkleRootAlreadySet(round); - } - - merkleRoots[round] = merkleRoot; - emit MerkleRootSet(round, merkleRoot); - } - - ////////////////////////////// Internal Methods ////////////////////////////// - - function _getMerkleLeaf(address account, uint256 amount) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encode(account, amount)))); - } - - function _claim(AirdropClaim calldata airdropClaim) internal { - address account = msg.sender; - bytes32 merkleRoot = merkleRoots[airdropClaim.round]; - - // Throws an error if the merkle root is not set for the selected round - if (merkleRoot == bytes32(0)) { - revert MerkleRootNotSet(airdropClaim.round); - } - - // Revert if the user has already claimed - if (hasClaimed[airdropClaim.round][account]) { - revert AlreadyClaimed(airdropClaim.round, account); - } - - // Verify the merkle proof - bytes32 leaf = _getMerkleLeaf(account, airdropClaim.amount); - if (!MerkleProof.verify(airdropClaim.merkleProof, merkleRoot, leaf)) { - revert InvalidProof(); - } - - // Reentrancy guard - hasClaimed[airdropClaim.round][account] = true; - emit Claimed(airdropClaim.round, account, airdropClaim.amount); - - // Transfer tokens to claimer - AIRDROP_TOKEN.safeTransfer(account, airdropClaim.amount); - } -} diff --git a/src/MemoryDistributor.sol b/src/MemoryDistributor.sol new file mode 100644 index 0000000..091c22c --- /dev/null +++ b/src/MemoryDistributor.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.25; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +struct AirdropClaim { + uint256 round; + uint256 amount; + bytes32[] merkleProof; +} + +contract MemoryDistributor is Ownable, ReentrancyGuard { + ////////////////////////////// Libraries ////////////////////////////// + + using SafeERC20 for IERC20; // Prevent sending tokens to recipients who can’t receive + + ////////////////////////////// Constants ////////////////////////////// + uint8 public constant MAX_CLAIMS_PER_TX = 100; + + ////////////////////////////// State ////////////////////////////// + + IERC20 public immutable MEM_TOKEN; + mapping(uint256 round => bytes32 merkleRoot) public merkleRoots; + mapping(uint256 round => mapping(address account => bool claimed)) public hasClaimed; + + ////////////////////////////// Errors ///////////////////////////// + + error InvalidMerkleRoot(); + error MerkleRootNotSet(uint256 round); + error MerkleRootAlreadySet(uint256 round); + error AlreadyClaimed(uint256 round, address account); + error InvalidProof(); + error EmptyAirdropClaim(); + error TooManyClaims(); + error InvalidEthTransfer(); + + ////////////////////////////// Events ///////////////////////////// + + event Claimed(uint256 indexed round, address account, uint256 amount); + event MerkleRootSet(uint256 indexed round, bytes32 merkleRoot); + event TokensWithdrawn(address indexed token, uint256 amount); + + ////////////////////////////// Constructor ///////////////////////////// + + constructor(address _initialOwner, address _memToken) Ownable(_initialOwner) { + MEM_TOKEN = IERC20(_memToken); + } + + ////////////////////////////// External Methods ////////////////////////////// + + function claim(AirdropClaim[] calldata _airdropClaims) external nonReentrant { + uint256 claimsLength = _airdropClaims.length; + + // Revert if the airdrop claims are empty + if (claimsLength == 0) revert EmptyAirdropClaim(); + if (claimsLength > MAX_CLAIMS_PER_TX) revert TooManyClaims(); + + for (uint256 i = 0; i < _airdropClaims.length;) { + _claim(_airdropClaims[i]); + unchecked { + ++i; + } + } + } + + function setRound(uint256 _round, bytes32 _merkleRoot) external onlyOwner { + // Merkle root must not be empty + if (_merkleRoot == bytes32(0)) { + revert InvalidMerkleRoot(); + } + + // Round must not be set yet + if (merkleRoots[_round] != bytes32(0)) { + revert MerkleRootAlreadySet(_round); + } + + merkleRoots[_round] = _merkleRoot; + emit MerkleRootSet(_round, _merkleRoot); + } + + function withdraw(address token, uint256 amount) external onlyOwner { + if (token == address(0)) { + (bool success,) = payable(owner()).call{ value: amount }(""); + if (!success) { + revert InvalidEthTransfer(); + } + } else { + IERC20(token).safeTransfer(owner(), amount); + } + emit TokensWithdrawn(token, amount); + } + + receive() external payable { } + + ////////////////////////////// Internal Methods ////////////////////////////// + + function _getMerkleLeaf(address _account, uint256 _amount) internal pure returns (bytes32) { + return keccak256(bytes.concat(keccak256(abi.encode(_account, _amount)))); + } + + function _claim(AirdropClaim calldata _airdropClaim) internal { + address account = msg.sender; + bytes32 merkleRoot = merkleRoots[_airdropClaim.round]; + + // Throws an error if the merkle root is not set for the selected round + if (merkleRoot == bytes32(0)) { + revert MerkleRootNotSet(_airdropClaim.round); + } + + // Revert if the user has already claimed + if (hasClaimed[_airdropClaim.round][account]) { + revert AlreadyClaimed(_airdropClaim.round, account); + } + + // Verify the merkle proof + bytes32 leaf = _getMerkleLeaf(account, _airdropClaim.amount); + if (!MerkleProof.verify(_airdropClaim.merkleProof, merkleRoot, leaf)) { + revert InvalidProof(); + } + + hasClaimed[_airdropClaim.round][account] = true; + emit Claimed(_airdropClaim.round, account, _airdropClaim.amount); + + // Transfer tokens to claimer + MEM_TOKEN.safeTransfer(account, _airdropClaim.amount); + } +} diff --git a/src/MemoryFeeRouter.sol b/src/MemoryFeeRouter.sol new file mode 100644 index 0000000..e58510d --- /dev/null +++ b/src/MemoryFeeRouter.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.25; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MemoryFeeRouter is Ownable, ReentrancyGuard { + ////////////////////////////// Libraries ////////////////////////////// + + using SafeERC20 for IERC20; // Prevent sending tokens to recipients who can’t receive + using Math for uint256; + + ////////////////////////////// Constants ////////////////////////////// + uint16 public constant MAX_PERCENTAGE = 10_000; + + ////////////////////////////// State ////////////////////////////// + + // Memory treasury percentage in 0.01% units + uint16 public memoryTreasuryPercentage; + address public memoryTreasury; + address public memoryDistributor; + IERC20 public immutable MEM_TOKEN; + + mapping(bytes32 id => bool supplied) public supplied; + + ////////////////////////////// Errors ///////////////////////////// + + error AlreadySupplied(bytes32 id); + error InvalidMemoryTreasuryPercentage(); + error InvalidSupplyAmount(); + + ////////////////////////////// Events ///////////////////////////// + + event Supplied(bytes32 indexed id, address indexed sender, uint256 amount); + event MemoryDistributorUpdated(address indexed newDistributor); + event MemoryTreasuryUpdated(address indexed newTreasury); + event MemoryTreasuryPercentageUpdated(uint16 newPercentage); + + ////////////////////////////// Constructor ///////////////////////////// + + constructor( + address _initialOwner, + address _memToken, + address _memoryTreasury, + address _memoryDistributor, + uint16 _memoryTreasuryPercentage + ) + Ownable(_initialOwner) + { + MEM_TOKEN = IERC20(_memToken); + _updateMemoryDistributor(_memoryDistributor); + _updateMemoryTreasury(_memoryTreasury); + _updateMemoryTreasuryPercentage(_memoryTreasuryPercentage); + } + + ////////////////////////////// External Methods ////////////////////////////// + + function updateMemoryTreasuryPercentage(uint16 _memoryTreasuryPercentage) external onlyOwner { + _updateMemoryTreasuryPercentage(_memoryTreasuryPercentage); + } + + function updateMemoryDistributor(address _memoryDistributor) external onlyOwner { + _updateMemoryDistributor(_memoryDistributor); + } + + function updateMemoryTreasury(address _memoryTreasury) external onlyOwner { + _updateMemoryTreasury(_memoryTreasury); + } + + function supply(uint256 _amount, bytes32 _id) external nonReentrant { + if (_amount == 0) revert InvalidSupplyAmount(); + _supply(_amount, _id); + } + + ////////////////////////////// Internal Methods ////////////////////////////// + + function _updateMemoryDistributor(address _memoryDistributor) internal { + memoryDistributor = _memoryDistributor; + emit MemoryDistributorUpdated(_memoryDistributor); + } + + function _updateMemoryTreasury(address _memoryTreasury) internal { + memoryTreasury = _memoryTreasury; + emit MemoryTreasuryUpdated(_memoryTreasury); + } + + function _updateMemoryTreasuryPercentage(uint16 _memoryTreasuryPercentage) internal { + if (_memoryTreasuryPercentage > MAX_PERCENTAGE) { + // Memory Treasury percentage can't be over 100% + revert InvalidMemoryTreasuryPercentage(); + } + + memoryTreasuryPercentage = _memoryTreasuryPercentage; + emit MemoryTreasuryPercentageUpdated(_memoryTreasuryPercentage); + } + + function _supply(uint256 _amount, bytes32 _id) internal { + if (supplied[_id]) { + revert AlreadySupplied(_id); + } + supplied[_id] = true; + + // Use mulDiv to prevent overflow + uint256 memoryTreasuryAmount = _amount.mulDiv(memoryTreasuryPercentage, MAX_PERCENTAGE); + + MEM_TOKEN.safeTransferFrom(msg.sender, memoryTreasury, memoryTreasuryAmount); + MEM_TOKEN.safeTransferFrom(msg.sender, memoryDistributor, _amount - memoryTreasuryAmount); + + emit Supplied(_id, msg.sender, _amount); + } +} diff --git a/tests/e2e/MemoryFeeRouter.t.sol b/tests/e2e/MemoryFeeRouter.t.sol new file mode 100644 index 0000000..434bbf0 --- /dev/null +++ b/tests/e2e/MemoryFeeRouter.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import { MEM } from "tests/utils/Mem.sol"; +import { BaseTest } from "tests/utils/BaseTest.t.sol"; +import { MemoryDistributor } from "src/MemoryDistributor.sol"; +import { MemoryFeeRouter } from "src/MemoryFeeRouter.sol"; + +contract MemoryFeeRouter_E2E_Test is BaseTest { + ////////////////////////////// State ////////////////////////////// + + // Addresses + address public owner = makeAddr("owner"); + address public memoryTreasury = makeAddr("MemoryTreasury"); + address public consumer1 = 0x0000000000000000000000000000000000000001; + address public consumer2 = 0x0000000000000000000000000000000000000002; + + // Parameters + // Initial memory treasury percentage of 80% + uint16 public memoryTreasuryPercentage = 8000; + + // Contracts + MemoryDistributor public memoryDistributor; + MemoryFeeRouter public memoryFeeRouter; + MEM public mem; + + ////////////////////////////// Setup ////////////////////////////// + function setUp() public { + // Deploy contracts + mem = new MEM(); + vm.label(address(mem), "MEM"); + + memoryDistributor = new MemoryDistributor(owner, address(mem)); + vm.label(address(memoryDistributor), "MemoryDistributor"); + + memoryFeeRouter = new MemoryFeeRouter( + owner, address(mem), memoryTreasury, address(memoryDistributor), memoryTreasuryPercentage + ); + vm.label(address(memoryFeeRouter), "MemoryFeeRouter"); + } + + ////////////////////////////// Utils ////////////////////////////// + + function _fundMem(address _receiver, uint256 _amount) internal { + mem.mint(_receiver, _amount); + } + + function _approveMem(address _approver, address _receiver, uint256 _amount) internal { + vm.prank(_approver); + mem.approve(_receiver, _amount); + } + + function _fundAndApproveMem(address _approver, address _receiver, uint256 _amount) internal { + _fundMem(_approver, _amount); + _approveMem(_approver, _receiver, _amount); + } + + function _getFeeRouterSplit( + uint256 _amount, + uint16 _memoryTreasuryPercentage + ) + internal + view + returns (uint256 _memoryTreasuryAmount, uint256 _memoryDistributorAmount) + { + _memoryTreasuryAmount = (_amount / memoryFeeRouter.MAX_PERCENTAGE()) * _memoryTreasuryPercentage; + + // Handle the remainder to ensure precise calculation + uint256 remainder = _amount % memoryFeeRouter.MAX_PERCENTAGE(); + _memoryTreasuryAmount += (remainder * _memoryTreasuryPercentage) / memoryFeeRouter.MAX_PERCENTAGE(); + + _memoryDistributorAmount = _amount - _memoryTreasuryAmount; + } + ////////////////////////////// Tests ////////////////////////////// + + function testFuzz_MemoryFeeRouter_SupplyFlow( + uint256 _consumer1Amount, + uint256 _consumer2Amount, + uint16 _memoryTreasuryPercentage + ) + external + { + _consumer1Amount = bound(_consumer1Amount, 1, type(uint256).max / 2); + _consumer2Amount = bound(_consumer2Amount, 1, type(uint256).max / 2); + _memoryTreasuryPercentage = uint16(bound(_memoryTreasuryPercentage, 0, memoryFeeRouter.MAX_PERCENTAGE())); + vm.assume(_memoryTreasuryPercentage != 0); + + // Set Memory Treasury Percentage + vm.prank(owner); + memoryFeeRouter.updateMemoryTreasuryPercentage(_memoryTreasuryPercentage); + + uint256 initialMemoryFeeRouterBalance = mem.balanceOf(address(memoryFeeRouter)); + uint256 initialMemoryDistributorBalance = mem.balanceOf(address(memoryDistributor)); + uint256 initialMemoryTreasuryBalance = mem.balanceOf(memoryTreasury); + + // Consumer 1 funds + _fundAndApproveMem(consumer1, address(memoryFeeRouter), _consumer1Amount); + + vm.prank(consumer1); + memoryFeeRouter.supply(_consumer1Amount, bytes32(0)); + + uint256 memoryFeeRouterBalanceAfterC1Supply = mem.balanceOf(address(memoryFeeRouter)); + uint256 memoryDistributorBalanceAfterC1Supply = mem.balanceOf(address(memoryDistributor)); + uint256 memoryTreasuryBalanceAfterC1Supply = mem.balanceOf(memoryTreasury); + (uint256 consumer1MemoryTreasuryAmount, uint256 consumer1MemoryDistributorAmount) = + _getFeeRouterSplit(_consumer1Amount, _memoryTreasuryPercentage); + + // Ensure balances are correct after consumer1 supplies + assertEq(memoryFeeRouterBalanceAfterC1Supply, initialMemoryFeeRouterBalance); + assertEq( + memoryDistributorBalanceAfterC1Supply, initialMemoryDistributorBalance + consumer1MemoryDistributorAmount + ); + assertEq(memoryTreasuryBalanceAfterC1Supply, initialMemoryTreasuryBalance + consumer1MemoryTreasuryAmount); + + // Consumer 2 funds + _fundAndApproveMem(consumer2, address(memoryFeeRouter), _consumer2Amount); + + vm.prank(consumer2); + memoryFeeRouter.supply(_consumer2Amount, bytes32(uint256(1))); + + uint256 memoryFeeRouterBalanceAfterC2Supply = mem.balanceOf(address(memoryFeeRouter)); + uint256 memoryDistributorBalanceAfterC2Supply = mem.balanceOf(address(memoryDistributor)); + uint256 memoryTreasuryBalanceAfterC2Supply = mem.balanceOf(memoryTreasury); + (uint256 consumer2MemoryTreasuryAmount, uint256 consumer2MemoryDistributorAmount) = + _getFeeRouterSplit(_consumer2Amount, _memoryTreasuryPercentage); + + // Ensure balances are correct after consumer2 supplies + assertEq(memoryFeeRouterBalanceAfterC2Supply, memoryFeeRouterBalanceAfterC1Supply); + assertEq( + memoryDistributorBalanceAfterC2Supply, + memoryDistributorBalanceAfterC1Supply + consumer2MemoryDistributorAmount + ); + assertEq(memoryTreasuryBalanceAfterC2Supply, memoryTreasuryBalanceAfterC1Supply + consumer2MemoryTreasuryAmount); + } +} diff --git a/tests/unit/FeeRouterEarningsAirdrop.t.sol b/tests/unit/MemoryDistributor.t.sol similarity index 58% rename from tests/unit/FeeRouterEarningsAirdrop.t.sol rename to tests/unit/MemoryDistributor.t.sol index b7f9f9d..cd0a124 100644 --- a/tests/unit/FeeRouterEarningsAirdrop.t.sol +++ b/tests/unit/MemoryDistributor.t.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.25; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { USDC } from "tests/utils/Usdc.sol"; +import { MEM } from "tests/utils/Mem.sol"; import { BaseTest } from "tests/utils/BaseTest.t.sol"; -import { FeeRouterEarningsAirdrop, AirdropClaim } from "src/FeeRouterEarningsAirdrop.sol"; -import { FeeRouterEarningsAirdropMock } from "tests/utils/FeeRouterEarningsAirdropMock.sol"; +import { MemoryDistributor, AirdropClaim } from "src/MemoryDistributor.sol"; +import { MemoryDistributorMock } from "tests/utils/MemoryDistributorMock.sol"; -contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { +contract MemoryDistributor_Unit_Test is BaseTest { ////////////////////////////// State ////////////////////////////// // Addresses @@ -18,8 +18,8 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { address public claimer4 = 0x4444444444444444444444444444444444444444; // Contracts - FeeRouterEarningsAirdropMock public feeRouterEarningsAirdrop; - USDC public usdc; + MemoryDistributorMock public memoryDistributor; + MEM public mem; ////////////////////////////// Setup ////////////////////////////// function setUp() public { @@ -30,13 +30,13 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { vm.label(claimer4, "Claimer 4"); // Deploy contracts - usdc = new USDC(); - vm.label(address(usdc), "USDC"); - feeRouterEarningsAirdrop = new FeeRouterEarningsAirdropMock(owner, address(usdc)); - vm.label(address(feeRouterEarningsAirdrop), "FeeRouterEarningsAirdrop"); + mem = new MEM(); + vm.label(address(mem), "MEM"); + memoryDistributor = new MemoryDistributorMock(owner, address(mem)); + vm.label(address(memoryDistributor), "MemoryDistributor"); - // Fund Fee Router - usdc.mint(address(feeRouterEarningsAirdrop), 1_000_000e6); + // Fund Memory Distributor + mem.mint(address(memoryDistributor), 1_000_000e6); } ////////////////////////////// Utils ////////////////////////////// @@ -44,7 +44,7 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { function _setRound(uint256 round, bytes32 merkleRoot) internal { vm.assume(merkleRoot != bytes32(0)); vm.prank(owner); - feeRouterEarningsAirdrop.setRound(round, merkleRoot); + memoryDistributor.setRound(round, merkleRoot); } function _getLeaf(uint256 round, address account, uint256 amount) internal pure returns (bytes32 leaf) { @@ -53,7 +53,15 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { ////////////////////////////// Success Tests ////////////////////////////// - function test_FeeRouterEarningsAirdrop_claim_userCanClaimBatchRounds() external { + function testFuzz_MemoryDistributor_constructor_memTokenAndOwnerSet(address _owner, address _memToken) external { + vm.assume(_owner != address(0)); + MemoryDistributorMock _memoryDistributor = new MemoryDistributorMock(_owner, _memToken); + + assertEq(_owner, _memoryDistributor.owner()); + assertEq(_memToken, address(_memoryDistributor.MEM_TOKEN())); + } + + function test_MemoryDistributor_claim_userCanClaimBatchRounds() external { // Rounds: // // [ @@ -73,7 +81,7 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { _setRound(0, merkleRootRound0); _setRound(1, merkleRootRound1); - uint256 initialClaimer1Balance = usdc.balanceOf(claimer1); + uint256 initialClaimer1Balance = mem.balanceOf(claimer1); // Claimer 1 claims rounds 0 and 1 in a single batch call uint256 claimer1Round0Amount = 500_000_000; @@ -91,15 +99,15 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { claimer1Claim[1] = AirdropClaim({ round: 1, amount: claimer1Round1Amount, merkleProof: claimer1Round1Proof }); vm.prank(claimer1); - feeRouterEarningsAirdrop.claim(claimer1Claim); + memoryDistributor.claim(claimer1Claim); - uint256 finalClaimer1Balance = usdc.balanceOf(claimer1); + uint256 finalClaimer1Balance = mem.balanceOf(claimer1); // Assert that claimer1's balance increased correctly assertEq(finalClaimer1Balance - initialClaimer1Balance, claimer1Round0Amount + claimer1Round1Amount); } - function test_FeeRouterEarningsAirdrop_claim_userCanClaim() external { + function test_MemoryDistributor_claim_userCanClaim() external { // Rounds: // // [ @@ -120,7 +128,7 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { _setRound(1, merkleRootRound1); // Claimer 1 claims rounds 0 and 1 - uint256 initialClaimer1UsdcBalance = usdc.balanceOf(claimer1); + uint256 initialClaimer1UsdcBalance = mem.balanceOf(claimer1); vm.startPrank(claimer1); uint256 claimer1Round0Amount = 500_000_000; bytes32[] memory claimer1Round0Proof = new bytes32[](2); @@ -130,9 +138,9 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { AirdropClaim[] memory claimer1Round0Claim = new AirdropClaim[](1); claimer1Round0Claim[0] = AirdropClaim({ round: 0, amount: claimer1Round0Amount, merkleProof: claimer1Round0Proof }); - feeRouterEarningsAirdrop.claim(claimer1Round0Claim); + memoryDistributor.claim(claimer1Round0Claim); - uint256 round0Claimer1UsdcBalance = usdc.balanceOf(claimer1); + uint256 round0Claimer1UsdcBalance = mem.balanceOf(claimer1); // Assert that claimer1's balance increased correctly assertEq(round0Claimer1UsdcBalance - initialClaimer1UsdcBalance, claimer1Round0Amount); @@ -144,14 +152,14 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { AirdropClaim[] memory claimer1Round1Claim = new AirdropClaim[](1); claimer1Round1Claim[0] = AirdropClaim({ round: 1, amount: claimer1Round1Amount, merkleProof: claimer1Round1Proof }); - feeRouterEarningsAirdrop.claim(claimer1Round1Claim); + memoryDistributor.claim(claimer1Round1Claim); vm.stopPrank(); - uint256 round1Claimer1UsdcBalance = usdc.balanceOf(claimer1); + uint256 round1Claimer1UsdcBalance = mem.balanceOf(claimer1); // Assert that claimer1's balance increased correctly assertEq(round1Claimer1UsdcBalance - round0Claimer1UsdcBalance, claimer1Round1Amount); // Claimer 4 claims round 1 - uint256 initialClaimer4UsdcBalance = usdc.balanceOf(claimer4); + uint256 initialClaimer4UsdcBalance = mem.balanceOf(claimer4); uint256 claimer4Round1Amount = 400_000_000; bytes32[] memory claimer4Round1Proof = new bytes32[](2); claimer4Round1Proof[0] = 0x81e631cb8bcea4c9b1e7805242d4ae38704c02046314ee4937595a47359f0c26; @@ -162,74 +170,93 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { AirdropClaim({ round: 1, amount: claimer4Round1Amount, merkleProof: claimer4Round1Proof }); vm.prank(claimer4); - feeRouterEarningsAirdrop.claim(claimer4Round1Claim); + memoryDistributor.claim(claimer4Round1Claim); - uint256 round1Claimer4UsdcBalance = usdc.balanceOf(claimer4); + uint256 round1Claimer4UsdcBalance = mem.balanceOf(claimer4); // Assert that claimer4's balance increased correctly assertEq(round1Claimer4UsdcBalance - initialClaimer4UsdcBalance, claimer4Round1Amount); } - function test_Fuzz_FeeRouterEarningsAirdrop_setRound_ownerCanSetRound(uint256 round, bytes32 merkleRoot) external { + function testFuzz_MemoryDistributor_setRound_ownerCanSetRound(uint256 round, bytes32 merkleRoot) external { // Assert that the round is not set - bytes32 initialMerkleRoot = feeRouterEarningsAirdrop.merkleRoots(round); + bytes32 initialMerkleRoot = memoryDistributor.merkleRoots(round); assertEq(initialMerkleRoot, bytes32(0)); // Owner sets the round _setRound(round, merkleRoot); // Assert that the round is set correctly - bytes32 finalMerkleRoot = feeRouterEarningsAirdrop.merkleRoots(round); + bytes32 finalMerkleRoot = memoryDistributor.merkleRoots(round); assertEq(finalMerkleRoot, merkleRoot); } - function test_Fuzz_FeeRouterEarningsAirdrop_getMerkleLeaf_compute(address account, uint256 amount) external view { - bytes32 actualMerkleLeaf = feeRouterEarningsAirdrop.exposed_getMerkleLeaf(account, amount); + function testFuzz_MemoryDistributor_getMerkleLeaf_compute(address account, uint256 amount) external view { + bytes32 actualMerkleLeaf = memoryDistributor.exposed_getMerkleLeaf(account, amount); bytes32 expectedMerkleLeaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount)))); assertEq(actualMerkleLeaf, expectedMerkleLeaf); } + function testFuzz_MemoryDistributor_withdraw_canWithdrawETH(uint256 amount) external { + uint256 initialOwnerBalance = owner.balance; + + vm.deal(address(memoryDistributor), amount); + vm.prank(owner); + vm.expectEmit(address(memoryDistributor)); + emit MemoryDistributor.TokensWithdrawn(address(0), amount); + memoryDistributor.withdraw(address(0), amount); + + uint256 finalOwnerBalance = owner.balance; + + assertEq(finalOwnerBalance, initialOwnerBalance + amount); + } + + function testFuzz_MemoryDistributor_withdraw_canWithdrawMEM(uint256 amount) external { + uint256 initialOwnerBalance = mem.balanceOf(owner); + amount = bound(amount, 0, mem.balanceOf(address(memoryDistributor))); + + mem.mint(address(memoryDistributor), amount); + + vm.prank(owner); + vm.expectEmit(address(memoryDistributor)); + emit MemoryDistributor.TokensWithdrawn(address(mem), amount); + memoryDistributor.withdraw(address(mem), amount); + + uint256 finalOwnerBalance = mem.balanceOf(owner); + + assertEq(finalOwnerBalance, initialOwnerBalance + amount); + } + // ////////////////////////////// Failure Tests ////////////////////////////// - function test_Fuzz_FeeRouterEarningsAirdrop_claim_RevertIf_MerkleRootNotSet( - uint256 round, - uint256 amount, - bytes32[] calldata merkleProof - ) - external - { - AirdropClaim[] memory airdropClaim = new AirdropClaim[](1); - airdropClaim[0] = AirdropClaim({ round: round, amount: amount, merkleProof: merkleProof }); - // Should revert if round not set - vm.expectRevert(abi.encodeWithSelector(FeeRouterEarningsAirdrop.MerkleRootNotSet.selector, round)); - feeRouterEarningsAirdrop.claim(airdropClaim); + function test_MemoryDistributor_constructor_RevertIf_OwnerIsZeroAddress() external { + address invalidOwner = address(0); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, invalidOwner)); + new MemoryDistributorMock(invalidOwner, address(0x11)); } - function test_Fuzz_FeeRouterEarningsAirdrop_claim_RevertIf_AlreadyClaimed( + function testFuzz_MemoryDistributor_claim_RevertIf_TooManyClaims( uint256 round, address account, - uint256 amount, bytes32 merkleRoot, - bytes32[] calldata merkleProof + uint8 airdropClaimLength ) external { // Setup Merkle Root for round _setRound(round, merkleRoot); - // Mock a claim - feeRouterEarningsAirdrop.mock_setHasClaimed(round, account, true); - - AirdropClaim[] memory airdropClaim = new AirdropClaim[](1); - airdropClaim[0] = AirdropClaim({ round: round, amount: amount, merkleProof: merkleProof }); + uint8 invalidAirdropClaimLength = + uint8(bound(uint256(airdropClaimLength), memoryDistributor.MAX_CLAIMS_PER_TX() + 1, type(uint8).max)); + AirdropClaim[] memory invalidAirdropClaim = new AirdropClaim[](invalidAirdropClaimLength); - // Should revert if account has already claimed for round - vm.expectRevert(abi.encodeWithSelector(FeeRouterEarningsAirdrop.AlreadyClaimed.selector, round, account)); - vm.prank(account); - feeRouterEarningsAirdrop.claim(airdropClaim); + // Should revert if the airdrop claims length is over the max claim amount + vm.expectRevert(MemoryDistributor.TooManyClaims.selector); + vm.startPrank(account); + memoryDistributor.claim(invalidAirdropClaim); } - function test_Fuzz_FeeRouterEarningsAirdrop_claim_RevertIf_EmptyAirdropClaim( + function testFuzz_MemoryDistributor_claim_RevertIf_EmptyAirdropClaim( uint256 round, address account, bytes32 merkleRoot @@ -242,12 +269,26 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { AirdropClaim[] memory airdropClaim = new AirdropClaim[](0); // Should revert if the airdrop claim is empty - vm.expectRevert(FeeRouterEarningsAirdrop.EmptyAirdropClaim.selector); + vm.expectRevert(MemoryDistributor.EmptyAirdropClaim.selector); vm.prank(account); - feeRouterEarningsAirdrop.claim(airdropClaim); + memoryDistributor.claim(airdropClaim); } - function test_Fuzz_FeeRouterEarningsAirdrop_claim_RevertIf_InvalidProof( + function testFuzz_MemoryDistributor_claim_RevertIf_MerkleRootNotSet( + uint256 round, + uint256 amount, + bytes32[] calldata merkleProof + ) + external + { + AirdropClaim[] memory airdropClaim = new AirdropClaim[](1); + airdropClaim[0] = AirdropClaim({ round: round, amount: amount, merkleProof: merkleProof }); + // Should revert if round not set + vm.expectRevert(abi.encodeWithSelector(MemoryDistributor.MerkleRootNotSet.selector, round)); + memoryDistributor.claim(airdropClaim); + } + + function testFuzz_MemoryDistributor_claim_RevertIf_AlreadyClaimed( uint256 round, address account, uint256 amount, @@ -259,34 +300,53 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { // Setup Merkle Root for round _setRound(round, merkleRoot); + // Mock a claim + memoryDistributor.mock_setHasClaimed(round, account, true); + AirdropClaim[] memory airdropClaim = new AirdropClaim[](1); airdropClaim[0] = AirdropClaim({ round: round, amount: amount, merkleProof: merkleProof }); - // Should revert if the proof is invalid - vm.expectRevert(FeeRouterEarningsAirdrop.InvalidProof.selector); + // Should revert if account has already claimed for round + vm.expectRevert(abi.encodeWithSelector(MemoryDistributor.AlreadyClaimed.selector, round, account)); vm.prank(account); - feeRouterEarningsAirdrop.claim(airdropClaim); + memoryDistributor.claim(airdropClaim); } - function test_Fuzz_FeeRouterEarningsAirdrop_setRound_RevertIf_NotOwner( + function testFuzz_MemoryDistributor_claim_RevertIf_InvalidProof( uint256 round, - bytes32 merkleRoot + address account, + uint256 amount, + bytes32 merkleRoot, + bytes32[] calldata merkleProof ) external { + // Setup Merkle Root for round + _setRound(round, merkleRoot); + + AirdropClaim[] memory airdropClaim = new AirdropClaim[](1); + airdropClaim[0] = AirdropClaim({ round: round, amount: amount, merkleProof: merkleProof }); + + // Should revert if the proof is invalid + vm.expectRevert(MemoryDistributor.InvalidProof.selector); + vm.prank(account); + memoryDistributor.claim(airdropClaim); + } + + function testFuzz_MemoryDistributor_setRound_RevertIf_NotOwner(uint256 round, bytes32 merkleRoot) external { // Should revert if a non-owner account tries to set the round vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); - feeRouterEarningsAirdrop.setRound(round, merkleRoot); + memoryDistributor.setRound(round, merkleRoot); } - function test_Fuzz_FeeRouterEarningsAirdrop_setRound_RevertIf_InvalidMerkleRoot(uint256 round) external { + function testFuzz_MemoryDistributor_setRound_RevertIf_InvalidMerkleRoot(uint256 round) external { // Should revert if the Merkle Root is empty - vm.expectRevert(FeeRouterEarningsAirdrop.InvalidMerkleRoot.selector); + vm.expectRevert(MemoryDistributor.InvalidMerkleRoot.selector); vm.prank(owner); - feeRouterEarningsAirdrop.setRound(round, bytes32(0)); + memoryDistributor.setRound(round, bytes32(0)); } - function test_Fuzz_FeeRouterEarningsAirdrop_setRound_RevertIf_MerkleRootAlreadySet( + function testFuzz_MemoryDistributor_setRound_RevertIf_MerkleRootAlreadySet( uint256 round, bytes32 merkleRoot ) @@ -296,7 +356,22 @@ contract FeeRouterEarningsAirdrop_Unit_Test is BaseTest { _setRound(round, merkleRoot); // Should revert if trying to set a Merkle Root that's already set - vm.expectRevert(abi.encodeWithSelector(FeeRouterEarningsAirdrop.MerkleRootAlreadySet.selector, round)); + vm.expectRevert(abi.encodeWithSelector(MemoryDistributor.MerkleRootAlreadySet.selector, round)); _setRound(round, merkleRoot); } + + function testFuzz_MemoryDistributor_withdraw_RevertIf_notOwner( + address _token, + uint256 _amount, + address _invalidOwner + ) + external + { + vm.assume(_invalidOwner != owner); + + // Should revert if account withdrawing is not the owner + vm.prank(_invalidOwner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _invalidOwner)); + memoryDistributor.withdraw(_token, _amount); + } } diff --git a/tests/unit/MemoryFeeRouter.t.sol b/tests/unit/MemoryFeeRouter.t.sol new file mode 100644 index 0000000..2e52c03 --- /dev/null +++ b/tests/unit/MemoryFeeRouter.t.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { MEM } from "tests/utils/Mem.sol"; +import { BaseTest } from "tests/utils/BaseTest.t.sol"; +import { MemoryFeeRouter } from "src/MemoryFeeRouter.sol"; + +contract MemoryFeeRouter_Unit_Test is BaseTest { + ////////////////////////////// State ////////////////////////////// + + // Addresses + address public owner = makeAddr("owner"); + address public memoryDistributor = makeAddr("MemoryDistributor"); + address public memoryTreasury = makeAddr("MemoryTreasury"); + + // Parameters + // Initial memory treasury percentage of 80% + uint16 public memoryTreasuryPercentage = 8000; + + // Contracts + MemoryFeeRouter public memoryFeeRouter; + MEM public mem; + + ////////////////////////////// Setup ////////////////////////////// + function setUp() public { + // Deploy contracts + mem = new MEM(); + vm.label(address(mem), "MEM"); + + memoryFeeRouter = + new MemoryFeeRouter(owner, address(mem), memoryTreasury, memoryDistributor, memoryTreasuryPercentage); + vm.label(address(memoryFeeRouter), "MemoryFeeRouter"); + } + + ////////////////////////////// Utils ////////////////////////////// + + function _fundMem(address _receiver, uint256 _amount) internal { + mem.mint(_receiver, _amount); + } + + function _approveMem(address _approver, address _receiver, uint256 _amount) internal { + vm.prank(_approver); + mem.approve(_receiver, _amount); + } + + function _fundAndApproveMem(address _approver, address _receiver, uint256 _amount) internal { + _fundMem(_approver, _amount); + _approveMem(_approver, _receiver, _amount); + } + ////////////////////////////// Success Tests ////////////////////////////// + + function testFuzz_MemoryFeeRouter_constructor_parametersSetCorrectly( + address _owner, + address _mem, + address _memoryTreasury, + address _memoryDistributor, + uint16 _memoryTreasuryPercentage + ) + external + { + // Setup bounds + vm.assume(_owner != address(0)); + _memoryTreasuryPercentage = uint16(bound(_memoryTreasuryPercentage, 0, 10_000)); + + MemoryFeeRouter _memoryFeeRouter = + new MemoryFeeRouter(_owner, _mem, _memoryTreasury, _memoryDistributor, _memoryTreasuryPercentage); + + assertEq(_owner, _memoryFeeRouter.owner()); + assertEq(_mem, address(_memoryFeeRouter.MEM_TOKEN())); + assertEq(_memoryTreasury, _memoryFeeRouter.memoryTreasury()); + assertEq(_memoryDistributor, _memoryFeeRouter.memoryDistributor()); + assertEq(_memoryTreasuryPercentage, _memoryFeeRouter.memoryTreasuryPercentage()); + } + + function testFuzz_MemoryFeeRouter_updateMemoryTreasuryPercentage_updatedCorrectly(uint16 _memoryTreasuryPercentage) + external + { + _memoryTreasuryPercentage = uint16(bound(_memoryTreasuryPercentage, 0, memoryFeeRouter.MAX_PERCENTAGE())); + vm.prank(owner); + vm.expectEmit(address(memoryFeeRouter)); + emit MemoryFeeRouter.MemoryTreasuryPercentageUpdated(_memoryTreasuryPercentage); + memoryFeeRouter.updateMemoryTreasuryPercentage(_memoryTreasuryPercentage); + assertEq(_memoryTreasuryPercentage, memoryFeeRouter.memoryTreasuryPercentage()); + } + + function testFuzz_MemoryFeeRouter_updateMemoryDistributor_updatedCorrectly(address _memoryDistributor) external { + vm.prank(owner); + vm.expectEmit(address(memoryFeeRouter)); + emit MemoryFeeRouter.MemoryDistributorUpdated(_memoryDistributor); + memoryFeeRouter.updateMemoryDistributor(_memoryDistributor); + assertEq(_memoryDistributor, memoryFeeRouter.memoryDistributor()); + } + + function testFuzz_MemoryFeeRouter_updateMemoryTreasury_updatedCorrectly(address _memoryTreasury) external { + vm.prank(owner); + vm.expectEmit(address(memoryFeeRouter)); + emit MemoryFeeRouter.MemoryTreasuryUpdated(_memoryTreasury); + memoryFeeRouter.updateMemoryTreasury(_memoryTreasury); + assertEq(_memoryTreasury, memoryFeeRouter.memoryTreasury()); + } + + function testFuzz_MemoryFeeRouter_supply_suppliedIsSetTrue( + address _supplier, + uint256 _amount, + bytes32 _id + ) + external + { + vm.assume(_amount != 0); + vm.assume(_supplier != address(0)); + + _fundAndApproveMem(_supplier, address(memoryFeeRouter), _amount); + + // Supplied should be false for a given _id + assertEq(memoryFeeRouter.supplied(_id), false); + + // Supply should work at the first time for a given id + vm.prank(_supplier); + memoryFeeRouter.supply(_amount, _id); + + // Supplied should be true after supplying + assertEq(memoryFeeRouter.supplied(_id), true); + } + + function testFuzz_MemoryFeeRouter_supply_suppliedIsEmitted( + address _supplier, + uint256 _amount, + bytes32 _id + ) + external + { + vm.assume(_amount != 0); + vm.assume(_supplier != address(0)); + + _fundAndApproveMem(_supplier, address(memoryFeeRouter), _amount); + + // Supply should work at the first time for a given id + vm.prank(_supplier); + vm.expectEmit(address(memoryFeeRouter)); + emit MemoryFeeRouter.Supplied(_id, _supplier, _amount); + memoryFeeRouter.supply(_amount, _id); + } + + function testFuzz_MemoryFeeRouter_supply_fundsDistributed( + address _supplier, + uint256 _amount, + bytes32 _id, + uint16 _memoryTreasuryPercentage + ) + external + { + vm.assume(_amount != 0); + vm.assume(_supplier != address(0)); + _memoryTreasuryPercentage = uint16(bound(_memoryTreasuryPercentage, 0, memoryFeeRouter.MAX_PERCENTAGE())); + + uint256 initialMemoryFeeRouterBalance = mem.balanceOf(address(memoryFeeRouter)); + uint256 initialMemoryTreasuryBalance = mem.balanceOf(address(memoryTreasury)); + uint256 initialMemoryDistributorBalance = mem.balanceOf(address(memoryDistributor)); + + // Set Memory Treasury Percentage + vm.prank(owner); + memoryFeeRouter.updateMemoryTreasuryPercentage(_memoryTreasuryPercentage); + + uint256 memoryTreasuryAmount = (_amount / memoryFeeRouter.MAX_PERCENTAGE()) * _memoryTreasuryPercentage; + + // Handle the remainder to ensure precise calculation + uint256 remainder = _amount % memoryFeeRouter.MAX_PERCENTAGE(); + memoryTreasuryAmount += (remainder * _memoryTreasuryPercentage) / memoryFeeRouter.MAX_PERCENTAGE(); + + uint256 memoryDistributorAmount = _amount - memoryTreasuryAmount; + + _fundAndApproveMem(_supplier, address(memoryFeeRouter), _amount); + + vm.prank(_supplier); + memoryFeeRouter.supply(_amount, _id); + + uint256 finalMemoryFeeRouterBalance = mem.balanceOf(address(memoryFeeRouter)); + uint256 finalMemoryTreasuryBalance = mem.balanceOf(address(memoryTreasury)); + uint256 finalMemoryDistributorBalance = mem.balanceOf(address(memoryDistributor)); + + // MemoryFeeRouter balance shouldn't change + assertEq(finalMemoryFeeRouterBalance, initialMemoryFeeRouterBalance); + // Funds should be distributed to memoryTreasury and memoryDistributor + assertEq(finalMemoryTreasuryBalance, initialMemoryTreasuryBalance + memoryTreasuryAmount); + assertEq(finalMemoryDistributorBalance, initialMemoryDistributorBalance + memoryDistributorAmount); + } + + ////////////////////////////// Failure Tests ////////////////////////////// + + function test_MemoryFeeRouter_constructor_RevertIf_OwnerIsZeroAddress() external { + address invalidOwner = address(0); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, invalidOwner)); + new MemoryFeeRouter(invalidOwner, address(mem), memoryTreasury, memoryDistributor, memoryTreasuryPercentage); + } + + function testFuzz_MemoryFeeRouter_constructor_RevertIf_InvalidMemoryTreasuryPercentage( + uint16 _memoryTreasuryPercentage + ) + external + { + uint16 invalidMemoryTreasuryPercentage = + uint16(bound(_memoryTreasuryPercentage, memoryFeeRouter.MAX_PERCENTAGE() + 1, type(uint16).max)); + vm.expectRevert(MemoryFeeRouter.InvalidMemoryTreasuryPercentage.selector); + new MemoryFeeRouter(owner, address(mem), memoryTreasury, memoryDistributor, invalidMemoryTreasuryPercentage); + } + + function testFuzz_MemoryFeeRouter_updateMemoryTreasuryPercentage_RevertIf_notOwner(address _invalidOwner) + external + { + vm.assume(_invalidOwner != owner); + vm.prank(_invalidOwner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _invalidOwner)); + memoryFeeRouter.updateMemoryTreasuryPercentage(memoryTreasuryPercentage); + } + + function testFuzz_MemoryFeeRouter_updateMemoryTreasuryPercentage_RevertIf_InvalidMemoryTreasuryPercentage( + uint16 _memoryTreasuryPercentage + ) + external + { + uint16 invalidMemoryPercentage = + uint16(bound(_memoryTreasuryPercentage, memoryFeeRouter.MAX_PERCENTAGE() + 1, type(uint16).max)); + + vm.prank(owner); + vm.expectRevert(MemoryFeeRouter.InvalidMemoryTreasuryPercentage.selector); + memoryFeeRouter.updateMemoryTreasuryPercentage(invalidMemoryPercentage); + } + + function testFuzz_MemoryFeeRouter_updateMemoryDistributor_RevertIf_notOwner(address _invalidOwner) external { + vm.assume(_invalidOwner != owner); + vm.prank(_invalidOwner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _invalidOwner)); + memoryFeeRouter.updateMemoryDistributor(memoryDistributor); + } + + function testFuzz_MemoryFeeRouter_updateMemoryTreasury_RevertIf_notOwner(address _invalidOwner) external { + vm.assume(_invalidOwner != owner); + vm.prank(_invalidOwner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _invalidOwner)); + memoryFeeRouter.updateMemoryTreasury(memoryTreasury); + } + + function testFuzz_MemoryFeeRouter_supply_RevertIf_InvalidSupplyAmount(bytes32 _id) external { + vm.expectRevert(MemoryFeeRouter.InvalidSupplyAmount.selector); + memoryFeeRouter.supply(0, _id); + } + + function testFuzz_MemoryFeeRouter_supply_RevertIf_AlreadySupplied( + address _supplier, + uint256 _amount, + bytes32 _id + ) + external + { + vm.assume(_supplier != address(0)); + _amount = bound(_amount, 1, type(uint256).max / 2); + + _fundAndApproveMem(_supplier, address(memoryFeeRouter), _amount); + + // Supply should work at the first time for a given id + vm.prank(_supplier); + memoryFeeRouter.supply(_amount, _id); + + _fundAndApproveMem(_supplier, address(memoryFeeRouter), _amount); + + // Supply should error if already supplied + vm.expectRevert(abi.encodeWithSelector(MemoryFeeRouter.AlreadySupplied.selector, _id)); + vm.prank(_supplier); + memoryFeeRouter.supply(_amount, _id); + } +} diff --git a/tests/utils/Usdc.sol b/tests/utils/Mem.sol similarity index 76% rename from tests/utils/Usdc.sol rename to tests/utils/Mem.sol index 0dfc4e1..ffec543 100644 --- a/tests/utils/Usdc.sol +++ b/tests/utils/Mem.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.25; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -contract USDC is ERC20 { - constructor() ERC20("USD Coin", "USDC") { } +contract MEM is ERC20 { + constructor() ERC20("Memory", "MEM") { } function decimals() public pure override returns (uint8) { - return 6; + return 18; } function mint(address account, uint256 amount) public { diff --git a/tests/utils/FeeRouterEarningsAirdropMock.sol b/tests/utils/MemoryDistributorMock.sol similarity index 69% rename from tests/utils/FeeRouterEarningsAirdropMock.sol rename to tests/utils/MemoryDistributorMock.sol index 7a4ccfe..3bc06c4 100644 --- a/tests/utils/FeeRouterEarningsAirdropMock.sol +++ b/tests/utils/MemoryDistributorMock.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.25; -import { FeeRouterEarningsAirdrop } from "src/FeeRouterEarningsAirdrop.sol"; +import { MemoryDistributor } from "src/MemoryDistributor.sol"; -contract FeeRouterEarningsAirdropMock is FeeRouterEarningsAirdrop { +contract MemoryDistributorMock is MemoryDistributor { ////////////////////////////// Constructor ////////////////////////////// - constructor(address _initialOwner, address _airdropToken) FeeRouterEarningsAirdrop(_initialOwner, _airdropToken) { } + constructor(address _initialOwner, address _airdropToken) MemoryDistributor(_initialOwner, _airdropToken) { } ////////////////////////////// Mock Methods //////////////////////////////