diff --git a/interface/IEscrow.sol b/interface/IEscrow.sol index 157f5f7..6eb971c 100644 --- a/interface/IEscrow.sol +++ b/interface/IEscrow.sol @@ -13,7 +13,8 @@ interface IEscrow { uint256 indexed instanceId, address indexed token, address sender, - uint256 amount, + uint256 netAmount, + uint256 feeAmount, bytes data ); @@ -109,7 +110,8 @@ interface IEscrow { event AddedDistributor(uint256 indexed repoId, uint256 indexed instanceId, address indexed distributor); event RemovedDistributor(uint256 indexed repoId, uint256 indexed instanceId, address indexed distributor); event BatchLimitSet(uint256 newBatchLimit); - event FeeSet(uint256 oldFee, uint256 newFee); + event FeeOnClaimSet(uint256 oldFee, uint256 newFee); + event FeeOnFundSet(uint256 oldFee, uint256 newFee); event FeeRecipientSet(address indexed oldRecipient, address indexed newRecipient); event SignerSet(address indexed oldSigner, address indexed newSigner); } \ No newline at end of file diff --git a/libraries/Params.sol b/libraries/Params.sol index d004fe1..80e69f5 100644 --- a/libraries/Params.sol +++ b/libraries/Params.sol @@ -11,7 +11,8 @@ library Params { address constant BASE_OWNER = 0x7163a6C74a3caB2A364F9aDD054bf83E50A1d8Bc; address constant BASE_SIGNER = 0x7F26a8d1A94bD7c1Db651306f503430dF37E9037; address constant BASE_USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; - uint constant BASE_FEE_BPS = 250; + uint constant BASE_FEE_ON_FUND_BPS = 0; + uint constant BASE_FEE_ON_CLAIM_BPS = 250; /*////////////////////////////////////////////////////////////// SEPOLIA @@ -23,7 +24,8 @@ library Params { address constant SEPOLIA_TESTER = 0x99ecA80b4Ebf8fDACe6627BEcb75EF1e620E6956; address constant SEPOLIA_TESTER_JSON = 0x5C87eA705eE49a96532F45f5db606A5f5fEF9780; address constant SEPOLIA_TESTER_SHAFU = 0x39053B170bBD9580d0b86e8317c685aEFB65f1ec; - uint constant SEPOLIA_FEE_BPS = 250; + uint constant SEPOLIA_FEE_ON_FUND_BPS = 0; + uint constant SEPOLIA_FEE_ON_CLAIM_BPS = 250; /*////////////////////////////////////////////////////////////// BASE SEPOLIA @@ -35,5 +37,6 @@ library Params { address constant BASESEPOLIA_TESTER = 0x5C87eA705eE49a96532F45f5db606A5f5fEF9780; address constant BASESEPOLIA_TESTER_SHAFU = 0x39053B170bBD9580d0b86e8317c685aEFB65f1ec; address constant BASESEPOLIA_TESTER_JSON = 0x5C87eA705eE49a96532F45f5db606A5f5fEF9780; - uint constant BASESEPOLIA_FEE_BPS = 250; + uint constant BASESEPOLIA_FEE_ON_FUND_BPS = 0; + uint constant BASESEPOLIA_FEE_ON_CLAIM_BPS = 250; } diff --git a/script/Deploy.Anvil.s.sol b/script/Deploy.Anvil.s.sol index e3a529e..be81d45 100644 --- a/script/Deploy.Anvil.s.sol +++ b/script/Deploy.Anvil.s.sol @@ -92,8 +92,11 @@ contract DeployAnvil is Script { MockERC20 token3 = new MockERC20("Test Token 3", "TKN3", 18); escrow.whitelistToken(address(token3)); - // Test FeeSet event - escrow.setFee(300); // 3% + // Test FeeOnClaimSet event + escrow.setFeeOnClaim(300); // 3% + + // Test FeeOnFundSet event + escrow.setFeeOnFund(100); // 1% // Test FeeRecipientSet event escrow.setFeeRecipient(USER1); @@ -1103,7 +1106,7 @@ contract DeployAnvil is Script { feeRates[4] = 250; // Back to 2.5% for (uint i = 0; i < feeRates.length; i++) { - escrow.setFee(feeRates[i]); + escrow.setFeeOnClaim(feeRates[i]); } // Test different fee recipients @@ -1461,7 +1464,7 @@ contract DeployAnvil is Script { extremeFees[7] = 250; // Back to 2.5% for (uint i = 0; i < extremeFees.length; i++) { - escrow.setFee(extremeFees[i]); + escrow.setFeeOnClaim(extremeFees[i]); } // Test rotating fee recipients diff --git a/script/Deploy.Base.s.sol b/script/Deploy.Base.s.sol index 970e909..cda1579 100644 --- a/script/Deploy.Base.s.sol +++ b/script/Deploy.Base.s.sol @@ -16,7 +16,7 @@ contract DeployBase is Deploy { Params.BASE_OWNER, Params.BASE_SIGNER, initialWhitelistedTokens, - Params.BASE_FEE_BPS, + Params.BASE_FEE_ON_CLAIM_BPS, Params.BATCH_LIMIT ); diff --git a/script/Deploy.BaseSepolia.s.sol b/script/Deploy.BaseSepolia.s.sol index fa87c61..8d9b392 100644 --- a/script/Deploy.BaseSepolia.s.sol +++ b/script/Deploy.BaseSepolia.s.sol @@ -23,7 +23,7 @@ contract DeployBaseSepolia is Deploy { Params.BASESEPOLIA_OWNER, Params.BASESEPOLIA_SIGNER, initialWhitelistedTokens, - Params.BASESEPOLIA_FEE_BPS, + Params.BASESEPOLIA_FEE_ON_CLAIM_BPS, Params.BATCH_LIMIT ); diff --git a/script/Deploy.Core.s.sol b/script/Deploy.Core.s.sol index 0b5f6db..1453de6 100644 --- a/script/Deploy.Core.s.sol +++ b/script/Deploy.Core.s.sol @@ -12,7 +12,7 @@ contract Deploy is Script { address owner, address signer, address[] memory initialWhitelistedTokens, - uint feeBps, + uint feeOnClaimBps, uint batchLimit ) public @@ -26,7 +26,7 @@ contract Deploy is Script { owner, signer, initialWhitelistedTokens, - feeBps, + feeOnClaimBps, batchLimit ); @@ -42,7 +42,7 @@ contract Deploy is Script { owner, signer, initialWhitelistedTokens, - feeBps, + feeOnClaimBps, batchLimit ); diff --git a/src/Escrow.sol b/src/Escrow.sol index 99dad5a..3ea79e1 100644 --- a/src/Escrow.sol +++ b/src/Escrow.sol @@ -85,7 +85,8 @@ contract Escrow is Owned, IEscrow { mapping(uint => mapping(uint => uint)) public repoSetAdminNonce; // repoId → instanceId → nonce mapping(address => uint) public recipientClaimNonce; // recipient → nonce - uint public fee; + uint public feeOnFund; + uint public feeOnClaim; address public feeRecipient; uint public batchLimit; @@ -125,14 +126,14 @@ contract Escrow is Owned, IEscrow { address _owner, address _signer, address[] memory _whitelistedTokens, - uint _fee, + uint _feeOnClaim, uint _batchLimit ) Owned(_owner) { - require(_fee <= MAX_FEE, Errors.INVALID_FEE); + require(_feeOnClaim <= MAX_FEE, Errors.INVALID_FEE); signer = _signer; feeRecipient = _owner; - fee = _fee; + feeOnClaim = _feeOnClaim; batchLimit = _batchLimit; INITIAL_CHAIN_ID = block.chainid; INITIAL_DOMAIN_SEPARATOR = _domainSeparator(); @@ -205,10 +206,17 @@ contract Escrow is Owned, IEscrow { token.safeTransferFrom(msg.sender, address(this), amount); - accounts[repoId][instanceId].balance[address(token)] += amount; - fundings[repoId][instanceId][address(token)][msg.sender] += amount; + uint feeAmount = amount.mulDivUp(feeOnFund, 10_000); + require(amount > feeAmount, Errors.INVALID_AMOUNT); - emit FundedRepo(repoId, instanceId, address(token), msg.sender, amount, data); + if (feeAmount > 0) token.safeTransfer(feeRecipient, feeAmount); + + uint netAmount = amount - feeAmount; + + accounts[repoId][instanceId].balance[address(token)] += netAmount; + fundings[repoId][instanceId][address(token)][msg.sender] += netAmount; + + emit FundedRepo(repoId, instanceId, address(token), msg.sender, netAmount, feeAmount, data); } /* -------------------------------------------------------------------------- */ @@ -305,8 +313,7 @@ contract Escrow is Owned, IEscrow { require(distribution.amount > 0, Errors.INVALID_AMOUNT); require(whitelistedTokens.contains(address(distribution.token)), Errors.INVALID_TOKEN); - // Validate that after fees, recipient will receive at least 1 wei - uint feeAmount = distribution.amount.mulDivUp(fee, 10_000); + uint feeAmount = distribution.amount.mulDivUp(feeOnClaim, 10_000); require(distribution.amount > feeAmount, Errors.INVALID_AMOUNT); distributionId = distributionCount++; @@ -320,7 +327,7 @@ contract Escrow is Owned, IEscrow { exists: true, _type: _type, payer: _type == DistributionType.Solo ? msg.sender : address(0), - fee: fee + fee: feeOnClaim }); } @@ -486,14 +493,24 @@ contract Escrow is Owned, IEscrow { emit WhitelistedToken(token); } - function setFee(uint newFee) + function setFeeOnFund(uint newFee) + external + onlyOwner + { + require(newFee <= MAX_FEE, Errors.INVALID_FEE); + uint oldFee = feeOnFund; + feeOnFund = newFee; + emit FeeOnFundSet(oldFee, newFee); + } + + function setFeeOnClaim(uint newFee) external onlyOwner { require(newFee <= MAX_FEE, Errors.INVALID_FEE); - uint oldFee = fee; - fee = newFee; - emit FeeSet(oldFee, newFee); + uint oldFee = feeOnClaim; + feeOnClaim = newFee; + emit FeeOnClaimSet(oldFee, newFee); } function setFeeRecipient(address newRec) diff --git a/test/01_Deploy.t.sol b/test/01_Deploy.t.sol index dd259e2..ffde66e 100644 --- a/test/01_Deploy.t.sol +++ b/test/01_Deploy.t.sol @@ -43,7 +43,8 @@ contract Deploy_Test is Base_Test { assertEq(deployedEscrow.owner(), Params.BASE_OWNER); assertEq(deployedEscrow.signer(), Params.BASE_SIGNER); assertEq(deployedEscrow.feeRecipient(), Params.BASE_OWNER); - assertEq(deployedEscrow.fee(), Params.BASE_FEE_BPS); + assertEq(deployedEscrow.feeOnClaim(), Params.BASE_FEE_ON_CLAIM_BPS); + assertEq(deployedEscrow.feeOnFund(), Params.BASE_FEE_ON_FUND_BPS); assertEq(deployedEscrow.batchLimit(), Params.BATCH_LIMIT); assertEq(deployedEscrow.repoSetAdminNonce(0, 0), 0); assertEq(deployedEscrow.batchCount(), 0); @@ -83,7 +84,8 @@ contract Deploy_Test is Base_Test { assertEq(deployedEscrow.owner(), testOwner); assertEq(deployedEscrow.signer(), testSigner); assertEq(deployedEscrow.feeRecipient(), testOwner); - assertEq(deployedEscrow.fee(), feeBps); + assertEq(deployedEscrow.feeOnClaim(), feeBps); + assertEq(deployedEscrow.feeOnFund(), Params.BASE_FEE_ON_FUND_BPS); assertEq(deployedEscrow.batchLimit(), batchLimit); assertEq(deployedEscrow.repoSetAdminNonce(0, 0), 0); assertEq(deployedEscrow.batchCount(), 0); @@ -131,7 +133,7 @@ contract Deploy_Test is Base_Test { batchLimit ); - assertEq(deployedEscrow.fee(), maxFeeBps); + assertEq(deployedEscrow.feeOnClaim(), maxFeeBps); } function test_deploy_revert_invalidFeeBps() public { @@ -312,7 +314,7 @@ contract Deploy_Test is Base_Test { assertEq(deployedEscrow.owner(), _owner); assertEq(deployedEscrow.signer(), _signer); assertEq(deployedEscrow.feeRecipient(), _owner); - assertEq(deployedEscrow.fee(), _feeBps); + assertEq(deployedEscrow.feeOnClaim(), _feeBps); assertEq(deployedEscrow.batchLimit(), _batchLimit); } diff --git a/test/03_FundRepo.t.sol b/test/03_FundRepo.t.sol index 1478e7e..ddf22f5 100644 --- a/test/03_FundRepo.t.sol +++ b/test/03_FundRepo.t.sol @@ -26,7 +26,7 @@ contract FundRepo_Test is Base_Test { uint256 initialAccountBalance = escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)); vm.expectEmit(true, true, true, true); - emit FundedRepo(REPO_ID, ACCOUNT_ID, address(wETH), alice, FUND_AMOUNT, ""); + emit FundedRepo(REPO_ID, ACCOUNT_ID, address(wETH), alice, FUND_AMOUNT, 0, ""); vm.prank(alice); escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, FUND_AMOUNT, ""); @@ -392,7 +392,7 @@ contract FundRepo_Test is Base_Test { uint256 amount = 100e18; vm.expectEmit(true, true, true, true); - emit FundedRepo(REPO_ID, ACCOUNT_ID, address(wETH), alice, amount, data); + emit FundedRepo(REPO_ID, ACCOUNT_ID, address(wETH), alice, amount, 0, data); vm.prank(alice); escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, amount, data); @@ -421,6 +421,320 @@ contract FundRepo_Test is Base_Test { assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), totalAmount); } + /* -------------------------------------------------------------------------- */ + /* FEE ON FUND TESTS */ + /* -------------------------------------------------------------------------- */ + + function test_fundRepo_withFee_basic() public { + // Set 5% fee on fund + vm.prank(owner); + escrow.setFeeOnFund(500); // 5% + + uint256 fundAmount = 1000e18; + uint256 expectedFee = (fundAmount * 500) / 10_000; // 5% = 50e18 + uint256 expectedNet = fundAmount - expectedFee; // 950e18 + + uint256 initialFeeRecipientBalance = wETH.balanceOf(owner); + uint256 initialEscrowBalance = wETH.balanceOf(address(escrow)); + + vm.expectEmit(true, true, true, true); + emit FundedRepo(REPO_ID, ACCOUNT_ID, address(wETH), alice, expectedNet, expectedFee, "fee test"); + + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, "fee test"); + + // Verify fee recipient received the fee + assertEq(wETH.balanceOf(owner), initialFeeRecipientBalance + expectedFee); + + // Verify escrow received net amount + assertEq(wETH.balanceOf(address(escrow)), initialEscrowBalance + expectedNet); + + // Verify account balance tracks net amount + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), expectedNet); + + // Verify funding tracks net amount (for reclaim purposes) + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), expectedNet); + } + + function test_fundRepo_withFee_zeroFee() public { + // Set 0% fee + vm.prank(owner); + escrow.setFeeOnFund(0); + + uint256 fundAmount = 1000e18; + + vm.expectEmit(true, true, true, true); + emit FundedRepo(REPO_ID, ACCOUNT_ID, address(wETH), alice, fundAmount, 0, ""); + + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, ""); + + // With 0% fee, full amount should go to account + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), fundAmount); + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), fundAmount); + } + + function test_fundRepo_withFee_maxFee() public { + // Set maximum fee (10%) + vm.prank(owner); + escrow.setFeeOnFund(1000); // 10% + + uint256 fundAmount = 1000e18; + uint256 expectedFee = (fundAmount * 1000) / 10_000; // 10% = 100e18 + uint256 expectedNet = fundAmount - expectedFee; // 900e18 + + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, ""); + + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), expectedNet); + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), expectedNet); + } + + function test_fundRepo_withFee_multipleFundings() public { + // Set 2.5% fee + vm.prank(owner); + escrow.setFeeOnFund(250); // 2.5% + + uint256 firstAmount = 1000e18; + uint256 secondAmount = 500e18; + + uint256 firstFee = (firstAmount * 250) / 10_000; // 25e18 + uint256 secondFee = (secondAmount * 250) / 10_000; // 12.5e18 + + uint256 firstNet = firstAmount - firstFee; // 975e18 + uint256 secondNet = secondAmount - secondFee; // 487.5e18 + + uint256 initialFeeBalance = wETH.balanceOf(owner); + + // First funding + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, firstAmount, ""); + + // Second funding + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, secondAmount, ""); + + // Verify cumulative amounts + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), firstNet + secondNet); + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), firstNet + secondNet); + assertEq(wETH.balanceOf(owner), initialFeeBalance + firstFee + secondFee); + } + + function test_fundRepo_withFee_differentUsers() public { + // Set 3% fee + vm.prank(owner); + escrow.setFeeOnFund(300); // 3% + + uint256 aliceAmount = 1000e18; + uint256 bobAmount = 2000e18; + + uint256 aliceFee = (aliceAmount * 300) / 10_000; // 30e18 + uint256 bobFee = (bobAmount * 300) / 10_000; // 60e18 + + uint256 aliceNet = aliceAmount - aliceFee; // 970e18 + uint256 bobNet = bobAmount - bobFee; // 1940e18 + + // Setup bob + wETH.mint(bob, bobAmount); + vm.prank(bob); + wETH.approve(address(escrow), bobAmount); + + uint256 initialFeeBalance = wETH.balanceOf(owner); + + // Alice funds + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, aliceAmount, ""); + + // Bob funds + vm.prank(bob); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, bobAmount, ""); + + // Verify individual tracking + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), aliceNet); + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), bob), bobNet); + + // Verify total account balance + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), aliceNet + bobNet); + + // Verify total fees collected + assertEq(wETH.balanceOf(owner), initialFeeBalance + aliceFee + bobFee); + } + + function test_fundRepo_withFee_reclaimNetAmount() public { + // Set 4% fee + vm.prank(owner); + escrow.setFeeOnFund(400); // 4% + + uint256 fundAmount = 1000e18; + uint256 expectedFee = (fundAmount * 400) / 10_000; // 40e18 + uint256 expectedNet = fundAmount - expectedFee; // 960e18 + + // Fund the repo + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, ""); + + // Verify alice can only reclaim the net amount + uint256 initialAliceBalance = wETH.balanceOf(alice); + + vm.prank(alice); + escrow.reclaimRepoFunds(REPO_ID, ACCOUNT_ID, address(wETH), expectedNet); + + // Alice should receive back the net amount (what she effectively contributed) + assertEq(wETH.balanceOf(alice), initialAliceBalance + expectedNet); + + // Account balance should be zero + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), 0); + + // Alice's funding tracking should be zero + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), 0); + } + + function test_fundRepo_withFee_cannotReclaimMoreThanNet() public { + // Set 5% fee + vm.prank(owner); + escrow.setFeeOnFund(500); // 5% + + uint256 fundAmount = 1000e18; + uint256 expectedNet = fundAmount - (fundAmount * 500) / 10_000; // 950e18 + + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, ""); + + // Try to reclaim original amount (should fail) + expectRevert(Errors.INSUFFICIENT_FUNDS); + vm.prank(alice); + escrow.reclaimRepoFunds(REPO_ID, ACCOUNT_ID, address(wETH), fundAmount); + + // Try to reclaim more than net (should fail) + expectRevert(Errors.INSUFFICIENT_FUNDS); + vm.prank(alice); + escrow.reclaimRepoFunds(REPO_ID, ACCOUNT_ID, address(wETH), expectedNet + 1); + } + + function test_fundRepo_withFee_dynamicFeeChanges() public { + uint256 fundAmount = 1000e18; + + // First funding with 2% fee + vm.prank(owner); + escrow.setFeeOnFund(200); // 2% + + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, ""); + + uint256 firstNet = fundAmount - (fundAmount * 200) / 10_000; // 980e18 + + // Change fee to 6% + vm.prank(owner); + escrow.setFeeOnFund(600); // 6% + + // Second funding with new fee + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, ""); + + uint256 secondNet = fundAmount - (fundAmount * 600) / 10_000; // 940e18 + + // Verify cumulative tracking + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), firstNet + secondNet); + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), firstNet + secondNet); + } + + function test_fundRepo_fuzz_withFees(uint16 feeRate, uint256 amount) public { + vm.assume(feeRate <= 1000); // Max 10% fee + vm.assume(amount > 0 && amount <= FUND_AMOUNT * 10); + + // Set the fee rate + vm.prank(owner); + escrow.setFeeOnFund(feeRate); + + // Use the same calculation as the contract (mulDivUp) + uint256 expectedFee = (amount * feeRate + 9999) / 10_000; // This simulates mulDivUp + vm.assume(amount > expectedFee); // Ensure net amount > 0 + uint256 expectedNet = amount - expectedFee; + + uint256 initialFeeBalance = wETH.balanceOf(owner); + + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, amount, ""); + + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), expectedNet); + assertEq(escrow.getFunding(REPO_ID, ACCOUNT_ID, address(wETH), alice), expectedNet); + assertEq(wETH.balanceOf(owner), initialFeeBalance + expectedFee); + } + + function test_fundRepo_withFee_revert_amountTooSmall() public { + // Set 10% fee (maximum) + vm.prank(owner); + escrow.setFeeOnFund(1000); + + // With 10% fee, we need amount where amount <= fee + // Since we use mulDivUp, let's use amount = 1 + // fee = (1 * 1000) / 10_000 = 0.1, but mulDivUp rounds up, so fee = 1 + // amount (1) == fee (1), so net would be 0, should revert + uint256 amount = 1; + + expectRevert(Errors.INVALID_AMOUNT); + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, amount, ""); + } + + function test_fundRepo_withFee_interactionWithDistributions() public { + // Set 3% fee on fund + vm.prank(owner); + escrow.setFeeOnFund(300); // 3% + + uint256 fundAmount = 1000e18; + uint256 expectedNet = fundAmount - (fundAmount * 300) / 10_000; // 970e18 + + // Fund the repo + vm.prank(alice); + escrow.fundRepo(REPO_ID, ACCOUNT_ID, wETH, fundAmount, ""); + + // Initialize repo to enable distributions + address[] memory admins = new address[](1); + admins[0] = alice; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + escrow.DOMAIN_SEPARATOR(), + keccak256(abi.encode( + escrow.SET_ADMIN_TYPEHASH(), + REPO_ID, + ACCOUNT_ID, + keccak256(abi.encode(admins)), + escrow.repoSetAdminNonce(REPO_ID, ACCOUNT_ID), + deadline + )) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + escrow.initRepo(REPO_ID, ACCOUNT_ID, admins, deadline, v, r, s); + + // Try to distribute more than net amount (should fail) + Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); + distributions[0] = Escrow.DistributionParams({ + amount: expectedNet + 1, + recipient: bob, + claimPeriod: 3600, + token: wETH + }); + + expectRevert(Errors.INSUFFICIENT_BALANCE); + vm.prank(alice); + escrow.distributeFromRepo(REPO_ID, ACCOUNT_ID, distributions, ""); + + // Distribute valid amount (should succeed) + distributions[0].amount = expectedNet / 2; // Half of net amount + + vm.prank(alice); + escrow.distributeFromRepo(REPO_ID, ACCOUNT_ID, distributions, ""); + + // Verify remaining balance + assertEq(escrow.getAccountBalance(REPO_ID, ACCOUNT_ID, address(wETH)), expectedNet - (expectedNet / 2)); + } + // Event for testing - event FundedRepo(uint256 indexed repoId, uint256 indexed instanceId, address indexed token, address sender, uint256 amount, bytes data); + event FundedRepo(uint256 indexed repoId, uint256 indexed instanceId, address indexed token, address sender, uint256 amount, uint256 feeAmount, bytes data); } \ No newline at end of file diff --git a/test/04_DistributeFromRepo.t.sol b/test/04_DistributeFromRepo.t.sol index 90b8594..3bfbed3 100644 --- a/test/04_DistributeFromRepo.t.sol +++ b/test/04_DistributeFromRepo.t.sol @@ -373,7 +373,7 @@ contract DistributeFromRepo_Test is Base_Test { vm.assume(amount1 + amount2 <= FUND_AMOUNT); // Add validation for fee edge case - ensure amounts are large enough - uint256 currentFee = escrow.fee(); + uint256 currentFee = escrow.feeOnClaim(); if (currentFee > 0) { // Ensure amounts are large enough to handle fees // For 10% max fee, amounts should be at least 100 to avoid fee >= amount @@ -477,7 +477,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_revert_feeExceedsAmount_maxFee() public { // Set fee to maximum (10%) vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% // Try to create distribution where fee would equal or exceed amount Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); @@ -497,7 +497,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_revert_feeEqualsAmount() public { // Set fee to maximum (10%) vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% // Try to create distribution where fee would equal amount // With mulDivUp, fee = (10 * 1000 + 9999) / 10000 = 1 @@ -517,7 +517,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_revert_feeExceedsAmount_edgeCase() public { // Set fee to maximum (10%) vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% // Create distribution where mulDivUp would make fee >= amount // For amount = 1: fee = mulDivUp(1, 1000, 10000) = (1 * 1000 + 9999) / 10000 = 1 @@ -538,7 +538,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_revert_feeExceedsAmount_smallAmounts() public { // Set moderate fee (5%) vm.prank(owner); - escrow.setFee(500); // 5% + escrow.setFeeOnClaim(500); // 5% // Test various small amounts that would cause issues uint256[] memory problematicAmounts = new uint256[](3); @@ -580,7 +580,7 @@ contract DistributeFromRepo_Test is Base_Test { // Set the fee rate vm.prank(owner); - escrow.setFee(feeRate); + escrow.setFeeOnClaim(feeRate); // Calculate expected fee using same logic as contract uint256 expectedFee = (amount * feeRate + 9999) / 10000; // mulDivUp equivalent @@ -612,7 +612,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_feeSnapshotAtCreation() public { // Test that fee is correctly snapshotted at distribution creation time vm.prank(owner); - escrow.setFee(500); // 5% + escrow.setFeeOnClaim(500); // 5% Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); distributions[0] = Escrow.DistributionParams({ @@ -635,7 +635,7 @@ contract DistributeFromRepo_Test is Base_Test { // Create first distribution with 2% fee vm.prank(owner); - escrow.setFee(200); + escrow.setFeeOnClaim(200); Escrow.DistributionParams[] memory distributions1 = new Escrow.DistributionParams[](1); distributions1[0] = Escrow.DistributionParams({ @@ -650,7 +650,7 @@ contract DistributeFromRepo_Test is Base_Test { // Change fee and create second distribution with 8% fee vm.prank(owner); - escrow.setFee(800); + escrow.setFeeOnClaim(800); Escrow.DistributionParams[] memory distributions2 = new Escrow.DistributionParams[](1); distributions2[0] = Escrow.DistributionParams({ @@ -674,7 +674,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_zeroFeeSnapshot() public { // Test that zero fees are correctly snapshotted vm.prank(owner); - escrow.setFee(0); // 0% fee + escrow.setFeeOnClaim(0); // 0% fee Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); distributions[0] = Escrow.DistributionParams({ @@ -694,7 +694,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_maxFeeSnapshot() public { // Test that maximum fees are correctly snapshotted vm.prank(owner); - escrow.setFee(1000); // 10% (maximum) fee + escrow.setFeeOnClaim(1000); // 10% (maximum) fee Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); distributions[0] = Escrow.DistributionParams({ @@ -714,7 +714,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_batchDistributionsSameFeeSnapshot() public { // Test that all distributions in a batch get the same fee snapshot vm.prank(owner); - escrow.setFee(300); // 3% + escrow.setFeeOnClaim(300); // 3% Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](3); distributions[0] = Escrow.DistributionParams({ @@ -749,7 +749,7 @@ contract DistributeFromRepo_Test is Base_Test { function test_distributeFromRepo_feeChangeAfterCreationDoesNotAffect() public { // Test that changing fee after creation doesn't affect existing distributions vm.prank(owner); - escrow.setFee(250); // 2.5% + escrow.setFeeOnClaim(250); // 2.5% Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); distributions[0] = Escrow.DistributionParams({ @@ -764,14 +764,14 @@ contract DistributeFromRepo_Test is Base_Test { // Change fee after creation vm.prank(owner); - escrow.setFee(750); // 7.5% + escrow.setFeeOnClaim(750); // 7.5% // Check that existing distribution still has original fee Escrow.Distribution memory distribution = escrow.getDistribution(distributionIds[0]); assertEq(distribution.fee, 250, "Existing distribution should retain original fee"); // Verify global fee did change - assertEq(escrow.fee(), 750, "Global fee should have changed"); + assertEq(escrow.feeOnClaim(), 750, "Global fee should have changed"); } function test_distributeFromSender_revert_invalidToken() public { @@ -1374,7 +1374,7 @@ contract DistributeFromRepo_Test is Base_Test { // Set fee rate vm.prank(owner); - escrow.setFee(feeRate); + escrow.setFeeOnClaim(feeRate); // Fund repo wETH.mint(address(this), amount); @@ -1522,7 +1522,7 @@ contract DistributeFromRepo_Test is Base_Test { Escrow.Distribution memory distribution = escrow.getDistribution(distributionIds[0]); assertEq(distribution.amount, DISTRIBUTION_AMOUNT); assertEq(distribution.recipient, recipient1); - assertEq(distribution.fee, escrow.fee()); // Should snapshot current fee + assertEq(distribution.fee, escrow.feeOnClaim()); // Should snapshot current fee // Claim the distribution uint256 deadline = block.timestamp + 1 hours; diff --git a/test/05_DistributeFromSender.t.sol b/test/05_DistributeFromSender.t.sol index 639472f..ca55cd6 100644 --- a/test/05_DistributeFromSender.t.sol +++ b/test/05_DistributeFromSender.t.sol @@ -342,7 +342,7 @@ contract DistributeFromSender_Test is Base_Test { vm.assume(amount2 > 0 && amount2 <= maxAmount); // Add validation for fee edge case - ensure amounts are large enough - uint256 currentFee = escrow.fee(); + uint256 currentFee = escrow.feeOnClaim(); if (currentFee > 0) { // Ensure amounts are large enough to handle fees // For 10% max fee, amounts should be at least 100 to avoid fee >= amount @@ -446,7 +446,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_revert_feeExceedsAmount_maxFee() public { // Set fee to maximum (10%) vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% // Create distribution where fee would equal or exceed amount Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); @@ -469,7 +469,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_revert_feeExceedsAmount_edgeCase() public { // Set fee to maximum (10%) vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% // Create distribution where mulDivUp would make fee >= amount // For amount = 1: fee = mulDivUp(1, 1000, 10000) = (1 * 1000 + 9999) / 10000 = 1 @@ -493,7 +493,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_revert_feeExceedsAmount_smallAmounts() public { // Set moderate fee (2.5%) vm.prank(owner); - escrow.setFee(250); // 2.5% + escrow.setFeeOnClaim(250); // 2.5% // Give distributor minimal tokens for this test wETH.mint(distributor, 1000); @@ -535,7 +535,7 @@ contract DistributeFromSender_Test is Base_Test { // Set the fee rate vm.prank(owner); - escrow.setFee(feeRate); + escrow.setFeeOnClaim(feeRate); // Calculate expected fee using same logic as contract uint256 expectedFee = (amount * feeRate + 9999) / 10000; // mulDivUp equivalent @@ -585,7 +585,7 @@ contract DistributeFromSender_Test is Base_Test { for (uint i = 0; i < feeRates.length; i++) { vm.prank(owner); - escrow.setFee(feeRates[i]); + escrow.setFeeOnClaim(feeRates[i]); Escrow.DistributionParams[] memory distributions = new Escrow.DistributionParams[](1); distributions[0] = Escrow.DistributionParams({ @@ -608,7 +608,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_feeSnapshotAtCreation() public { // Test that fee is correctly snapshotted at distribution creation time vm.prank(owner); - escrow.setFee(600); // 6% + escrow.setFeeOnClaim(600); // 6% wETH.mint(distributor, 1000e18); @@ -634,7 +634,7 @@ contract DistributeFromSender_Test is Base_Test { // Create first distribution with 3% fee vm.prank(owner); - escrow.setFee(300); + escrow.setFeeOnClaim(300); Escrow.DistributionParams[] memory distributions1 = new Escrow.DistributionParams[](1); distributions1[0] = Escrow.DistributionParams({ @@ -649,7 +649,7 @@ contract DistributeFromSender_Test is Base_Test { // Change fee and create second distribution with 7% fee vm.prank(owner); - escrow.setFee(700); + escrow.setFeeOnClaim(700); Escrow.DistributionParams[] memory distributions2 = new Escrow.DistributionParams[](1); distributions2[0] = Escrow.DistributionParams({ @@ -673,7 +673,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_zeroFeeSnapshot() public { // Test that zero fees are correctly snapshotted vm.prank(owner); - escrow.setFee(0); // 0% fee + escrow.setFeeOnClaim(0); // 0% fee wETH.mint(distributor, 1000e18); @@ -695,7 +695,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_maxFeeSnapshot() public { // Test that maximum fees are correctly snapshotted vm.prank(owner); - escrow.setFee(1000); // 10% (maximum) fee + escrow.setFeeOnClaim(1000); // 10% (maximum) fee wETH.mint(distributor, 1000e18); @@ -717,7 +717,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_batchDistributionsSameFeeSnapshot() public { // Test that all distributions in a batch get the same fee snapshot vm.prank(owner); - escrow.setFee(400); // 4% + escrow.setFeeOnClaim(400); // 4% wETH.mint(distributor, 5000e18); @@ -754,7 +754,7 @@ contract DistributeFromSender_Test is Base_Test { function test_distributeFromSender_feeChangeAfterCreationDoesNotAffect() public { // Test that changing fee after creation doesn't affect existing distributions vm.prank(owner); - escrow.setFee(150); // 1.5% + escrow.setFeeOnClaim(150); // 1.5% wETH.mint(distributor, 1000e18); @@ -771,14 +771,14 @@ contract DistributeFromSender_Test is Base_Test { // Change fee after creation vm.prank(owner); - escrow.setFee(850); // 8.5% + escrow.setFeeOnClaim(850); // 8.5% // Check that existing distribution still has original fee Escrow.Distribution memory distribution = escrow.getDistribution(distributionIds[0]); assertEq(distribution.fee, 150, "Existing distribution should retain original fee"); // Verify global fee did change - assertEq(escrow.fee(), 850, "Global fee should have changed"); + assertEq(escrow.feeOnClaim(), 850, "Global fee should have changed"); } function test_distributeFromSender_multiplePayers_differentFees() public { @@ -796,7 +796,7 @@ contract DistributeFromSender_Test is Base_Test { // Payer1 creates distribution with 2% fee vm.prank(owner); - escrow.setFee(200); + escrow.setFeeOnClaim(200); Escrow.DistributionParams[] memory distributions1 = new Escrow.DistributionParams[](1); distributions1[0] = Escrow.DistributionParams({ @@ -811,7 +811,7 @@ contract DistributeFromSender_Test is Base_Test { // Change fee, then payer2 creates distribution with 9% fee vm.prank(owner); - escrow.setFee(900); + escrow.setFeeOnClaim(900); Escrow.DistributionParams[] memory distributions2 = new Escrow.DistributionParams[](1); distributions2[0] = Escrow.DistributionParams({ diff --git a/test/06_Claim.t.sol b/test/06_Claim.t.sol index 3acbd03..4785598 100644 --- a/test/06_Claim.t.sol +++ b/test/06_Claim.t.sol @@ -124,7 +124,7 @@ contract Claim_Test is Base_Test { uint256 deadline = block.timestamp + 1 hours; (uint8 v, bytes32 r, bytes32 s) = _signClaim(distributionIds, recipient, deadline); - uint256 expectedFee = (DISTRIBUTION_AMOUNT * escrow.fee()) / 10000; + uint256 expectedFee = (DISTRIBUTION_AMOUNT * escrow.feeOnClaim()) / 10000; uint256 expectedNetAmount = DISTRIBUTION_AMOUNT - expectedFee; uint256 initialRecipientBalance = wETH.balanceOf(recipient); @@ -166,7 +166,7 @@ contract Claim_Test is Base_Test { (uint8 v, bytes32 r, bytes32 s) = _signClaim(distributionIds, recipient, deadline); uint256 totalAmount = amount1 + amount2 + amount3; - uint256 expectedFee = (totalAmount * escrow.fee()) / 10000; + uint256 expectedFee = (totalAmount * escrow.feeOnClaim()) / 10000; uint256 expectedNetAmount = totalAmount - expectedFee; uint256 initialRecipientBalance = wETH.balanceOf(recipient); @@ -187,7 +187,7 @@ contract Claim_Test is Base_Test { function test_claim_zeroFee() public { // Set fee to 0 vm.prank(owner); - escrow.setFee(0); + escrow.setFeeOnClaim(0); uint256 distributionId = _createRepoDistribution(recipient, DISTRIBUTION_AMOUNT); uint[] memory distributionIds = new uint[](1); @@ -356,7 +356,7 @@ contract Claim_Test is Base_Test { (uint8 v, bytes32 r, bytes32 s) = _signClaim(distributionIds, recipient, deadline); // Use the fee calculation logic from the contract - uint256 expectedFee = (amount * escrow.fee() + 9999) / 10000; // Round up like mulDivUp + uint256 expectedFee = (amount * escrow.feeOnClaim() + 9999) / 10000; // Round up like mulDivUp if (expectedFee >= amount) { expectedFee = amount - 1; // Cap fee to ensure recipient gets at least 1 wei } @@ -430,13 +430,13 @@ contract Claim_Test is Base_Test { // Create distribution with 10% fee vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% uint256 distributionId = _createRepoDistribution(recipient, 100); // 100 wei // Try to change fee after distribution - this should NOT affect the claim vm.prank(owner); - escrow.setFee(250); // 2.5% - lower fee + escrow.setFeeOnClaim(250); // 2.5% - lower fee uint[] memory distributionIds = new uint[](1); distributionIds[0] = distributionId; @@ -463,13 +463,13 @@ contract Claim_Test is Base_Test { // Create a small distribution with a reasonable fee vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% uint256 distributionId = _createRepoDistribution(recipient, 20); // 20 wei // Try to change fee to a different value after distribution vm.prank(owner); - escrow.setFee(500); // 5% - this should NOT affect the claim + escrow.setFeeOnClaim(500); // 5% - this should NOT affect the claim uint[] memory distributionIds = new uint[](1); distributionIds[0] = distributionId; @@ -500,7 +500,7 @@ contract Claim_Test is Base_Test { for (uint i = 0; i < feeRates.length; i++) { // Set fee rate vm.prank(owner); - escrow.setFee(uint16(feeRates[i])); + escrow.setFeeOnClaim(uint16(feeRates[i])); uint256 distributionId = _createRepoDistribution(recipient, DISTRIBUTION_AMOUNT); uint[] memory distributionIds = new uint[](1); @@ -567,13 +567,13 @@ contract Claim_Test is Base_Test { // Create distribution with maximum fee that still allows valid creation vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% uint256 distributionId = _createRepoDistribution(recipient, 10); // 10 wei // Try to change fee after distribution - should have no effect vm.prank(owner); - escrow.setFee(100); // 1% + escrow.setFeeOnClaim(100); // 1% uint[] memory distributionIds = new uint[](1); distributionIds[0] = distributionId; @@ -596,7 +596,7 @@ contract Claim_Test is Base_Test { // Ensure normal fee calculations work correctly with fee snapshotting vm.prank(owner); - escrow.setFee(250); // 2.5% + escrow.setFeeOnClaim(250); // 2.5% uint256 distributionId = _createRepoDistribution(recipient, 1000e18); // Large amount @@ -625,17 +625,17 @@ contract Claim_Test is Base_Test { // Create first distribution with 1% fee vm.prank(owner); - escrow.setFee(100); // 1% + escrow.setFeeOnClaim(100); // 1% uint256 distributionId1 = _createRepoDistribution(recipient, 1000e18); // Large // Change fee and create second distribution vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% uint256 distributionId2 = _createRepoDistribution(recipient, 50); // Small - 50 wei // Try to change fee again - should not affect existing distributions vm.prank(owner); - escrow.setFee(500); // 5% + escrow.setFeeOnClaim(500); // 5% uint[] memory distributionIds = new uint[](2); distributionIds[0] = distributionId1; @@ -665,13 +665,13 @@ contract Claim_Test is Base_Test { // Create distribution with the fuzzed fee rate vm.prank(owner); - escrow.setFee(feeRate); + escrow.setFeeOnClaim(feeRate); uint256 distributionId = _createRepoDistribution(recipient, distributionAmount); // Try to change fee after creation - should have no effect vm.prank(owner); - escrow.setFee(feeRate == 1000 ? 100 : 1000); // Set to different value + escrow.setFeeOnClaim(feeRate == 1000 ? 100 : 1000); // Set to different value uint[] memory distributionIds = new uint[](1); distributionIds[0] = distributionId; @@ -711,13 +711,13 @@ contract Claim_Test is Base_Test { // Create distribution with low fee (1%) vm.prank(owner); - escrow.setFee(100); // 1% + escrow.setFeeOnClaim(100); // 1% uint256 distributionId = _createRepoDistribution(recipient, 1000e18); // Malicious owner tries to increase fee to maximum before claim vm.prank(owner); - escrow.setFee(1000); // 10% - trying to extract 10x more fees + escrow.setFeeOnClaim(1000); // 10% - trying to extract 10x more fees uint[] memory distributionIds = new uint[](1); distributionIds[0] = distributionId; @@ -949,13 +949,13 @@ contract Claim_Test is Base_Test { function test_claim_integration_afterFeeChange() public { // Create distribution with initial fee vm.prank(owner); - escrow.setFee(100); // 1% + escrow.setFeeOnClaim(100); // 1% uint256 distributionId = _createRepoDistribution(recipient, DISTRIBUTION_AMOUNT); // Change fee after distribution creation vm.prank(owner); - escrow.setFee(500); // 5% + escrow.setFeeOnClaim(500); // 5% // Claim should use original fee from distribution creation time uint[] memory distributionIds = new uint[](1); @@ -1011,7 +1011,7 @@ contract Claim_Test is Base_Test { vm.prank(owner); escrow.setFeeRecipient(newFeeRecipient); - uint256 expectedFee = (DISTRIBUTION_AMOUNT * escrow.fee()) / 10000; + uint256 expectedFee = (DISTRIBUTION_AMOUNT * escrow.feeOnClaim()) / 10000; vm.prank(recipient); escrow.claim(distributionIds, deadline, v, r, s, ""); @@ -1036,7 +1036,7 @@ contract Claim_Test is Base_Test { (uint8 v, bytes32 r, bytes32 s) = _signClaim(distributionIds, recipient, deadline); uint256 totalAmount = DISTRIBUTION_AMOUNT * batchSize; - uint256 expectedFee = (totalAmount * escrow.fee()) / 10000; + uint256 expectedFee = (totalAmount * escrow.feeOnClaim()) / 10000; uint256 expectedNetAmount = totalAmount - expectedFee; uint256 initialRecipientBalance = wETH.balanceOf(recipient); @@ -1064,7 +1064,7 @@ contract Claim_Test is Base_Test { // We'll test the normal edge case scenario to show the defensive logic exists vm.prank(owner); - escrow.setFee(1000); // 10% fee (maximum allowed) + escrow.setFeeOnClaim(1000); // 10% fee (maximum allowed) // Create the smallest distribution that can pass validation with max fee // With 10% fee: for amount=11, fee = mulDivUp(11, 1000, 10000) = 2 @@ -1146,7 +1146,7 @@ contract Claim_Test is Base_Test { // Set fee for this distribution vm.prank(owner); - escrow.setFee(feeRates[i]); + escrow.setFeeOnClaim(feeRates[i]); // Create distribution with this fee rate distributionIds[i] = _createRepoDistribution(recipient, amounts[i]); @@ -1216,7 +1216,7 @@ contract Claim_Test is Base_Test { feeRate = uint16(bound(feeRate, 1, 1000)); // 0.01% to 10% vm.prank(owner); - escrow.setFee(feeRate); + escrow.setFeeOnClaim(feeRate); uint256 distributionId = _createRepoDistribution(recipient, amount); uint256[] memory distributionIds = new uint256[](1); @@ -1296,7 +1296,7 @@ contract Claim_Test is Base_Test { // Create distributions with different fee rates for (uint256 i = 0; i < 5; i++) { vm.prank(owner); - escrow.setFee(uint16(feeRates[i])); + escrow.setFeeOnClaim(uint16(feeRates[i])); distributionIds[i] = _createRepoDistribution(recipient, 1000e18); @@ -1307,7 +1307,7 @@ contract Claim_Test is Base_Test { // Change fee rate again (shouldn't affect existing distributions) vm.prank(owner); - escrow.setFee(750); // 7.5% + escrow.setFeeOnClaim(750); // 7.5% uint256 deadline = block.timestamp + 1 hours; (uint8 v, bytes32 r, bytes32 s) = _signClaim(distributionIds, recipient, deadline); diff --git a/test/10_OnlyOwner.t.sol b/test/10_OnlyOwner.t.sol index b93dae8..49be1c6 100644 --- a/test/10_OnlyOwner.t.sol +++ b/test/10_OnlyOwner.t.sol @@ -103,68 +103,68 @@ contract OnlyOwner_Test is Base_Test { /* SET FEE TESTS */ /* -------------------------------------------------------------------------- */ - function test_setFee_success() public { + function test_setFeeOnClaim_success() public { uint256 newFee = 500; // 5% - assertEq(escrow.fee(), 250); // Initial fee from setup + assertEq(escrow.feeOnClaim(), 250); // Initial fee from setup vm.prank(owner); - escrow.setFee(newFee); + escrow.setFeeOnClaim(newFee); - assertEq(escrow.fee(), newFee); + assertEq(escrow.feeOnClaim(), newFee); } - function test_setFee_zeroFee() public { + function test_setFeeOnClaim_zeroFee() public { vm.prank(owner); - escrow.setFee(0); + escrow.setFeeOnClaim(0); - assertEq(escrow.fee(), 0); + assertEq(escrow.feeOnClaim(), 0); } - function test_setFee_maxFee() public { + function test_setFeeOnClaim_maxFee() public { uint256 maxFee = escrow.MAX_FEE(); // 10% vm.prank(owner); - escrow.setFee(maxFee); + escrow.setFeeOnClaim(maxFee); - assertEq(escrow.fee(), maxFee); + assertEq(escrow.feeOnClaim(), maxFee); } - function test_setFee_revert_notOwner() public { + function test_setFeeOnClaim_revert_notOwner() public { expectRevert("UNAUTHORIZED"); vm.prank(unauthorized); - escrow.setFee(500); + escrow.setFeeOnClaim(500); } - function test_setFee_revert_exceedsMaxFee() public { + function test_setFeeOnClaim_revert_exceedsMaxFee() public { uint256 invalidFee = escrow.MAX_FEE() + 1; expectRevert(Errors.INVALID_FEE); vm.prank(owner); - escrow.setFee(invalidFee); + escrow.setFeeOnClaim(invalidFee); } - function test_setFee_fuzz(uint256 fee) public { + function test_setFeeOnClaim_fuzz(uint256 fee) public { vm.assume(fee <= escrow.MAX_FEE()); vm.prank(owner); - escrow.setFee(fee); + escrow.setFeeOnClaim(fee); - assertEq(escrow.fee(), fee); + assertEq(escrow.feeOnClaim(), fee); } - function test_setFee_emitsEvent() public { + function test_setFeeOnClaim_emitsEvent() public { uint256 newFee = 500; // 5% vm.expectEmit(true, true, true, true); - emit FeeSet(escrow.fee(), newFee); + emit FeeOnClaimSet(escrow.feeOnClaim(), newFee); vm.prank(owner); - escrow.setFee(newFee); + escrow.setFeeOnClaim(newFee); } /* -------------------------------------------------------------------------- */ /* SET FEE RECIPIENT TESTS */ /* -------------------------------------------------------------------------- */ - function test_setFeeRecipient_success() public { + function test_setFeeOnClaimRecipient_success() public { assertEq(escrow.feeRecipient(), owner); // Initial from setup vm.prank(owner); @@ -173,7 +173,7 @@ contract OnlyOwner_Test is Base_Test { assertEq(escrow.feeRecipient(), newRecipient); } - function test_setFeeRecipient_setToZeroAddress() public { + function test_setFeeOnClaimRecipient_setToZeroAddress() public { // Contract allows setting to zero address (might be intentional) vm.prank(owner); escrow.setFeeRecipient(address(0)); @@ -181,7 +181,7 @@ contract OnlyOwner_Test is Base_Test { assertEq(escrow.feeRecipient(), address(0)); } - function test_setFeeRecipient_setBackToOwner() public { + function test_setFeeOnClaimRecipient_setBackToOwner() public { vm.prank(owner); escrow.setFeeRecipient(newRecipient); @@ -191,13 +191,13 @@ contract OnlyOwner_Test is Base_Test { assertEq(escrow.feeRecipient(), owner); } - function test_setFeeRecipient_revert_notOwner() public { + function test_setFeeOnClaimRecipient_revert_notOwner() public { expectRevert("UNAUTHORIZED"); vm.prank(unauthorized); escrow.setFeeRecipient(newRecipient); } - function test_setFeeRecipient_emitsEvent() public { + function test_setFeeOnClaimRecipient_emitsEvent() public { address differentRecipient = makeAddr("differentRecipient"); vm.expectEmit(true, true, true, true); emit FeeRecipientSet(escrow.feeRecipient(), differentRecipient); @@ -345,7 +345,7 @@ contract OnlyOwner_Test is Base_Test { assertEq(finalTokens.length, initialCount + numTokens); } - function test_setFeeRecipient_fuzz(address recipient) public { + function test_setFeeOnClaimRecipient_fuzz(address recipient) public { vm.prank(owner); escrow.setFeeRecipient(recipient); @@ -369,13 +369,13 @@ contract OnlyOwner_Test is Base_Test { vm.assume(newBatchLimit > 0); vm.startPrank(owner); - escrow.setFee(newFee); + escrow.setFeeOnClaim(newFee); escrow.setFeeRecipient(newFeeRecipient); escrow.setSigner(fuzzSigner); escrow.setBatchLimit(newBatchLimit); vm.stopPrank(); - assertEq(escrow.fee(), newFee); + assertEq(escrow.feeOnClaim(), newFee); assertEq(escrow.feeRecipient(), newFeeRecipient); assertEq(escrow.signer(), fuzzSigner); assertEq(escrow.batchLimit(), newBatchLimit); @@ -389,7 +389,7 @@ contract OnlyOwner_Test is Base_Test { // Set initial fee vm.prank(owner); - escrow.setFee(oldFee); + escrow.setFeeOnClaim(oldFee); // Create distribution with old fee address recipient = makeAddr("recipient"); @@ -397,12 +397,12 @@ contract OnlyOwner_Test is Base_Test { // Change fee vm.prank(owner); - escrow.setFee(newFee); + escrow.setFeeOnClaim(newFee); // Verify distribution retains old fee but global fee is new Escrow.Distribution memory distribution = escrow.getDistribution(0); assertEq(distribution.fee, oldFee, "Distribution should retain creation-time fee"); - assertEq(escrow.fee(), newFee, "Global fee should be updated"); + assertEq(escrow.feeOnClaim(), newFee, "Global fee should be updated"); } /* -------------------------------------------------------------------------- */ @@ -415,14 +415,14 @@ contract OnlyOwner_Test is Base_Test { vm.startPrank(owner); escrow.whitelistToken(token1); escrow.whitelistToken(token2); - escrow.setFee(750); + escrow.setFeeOnClaim(750); escrow.setFeeRecipient(newRecipient); escrow.setSigner(newSignerAddr); escrow.setBatchLimit(25); vm.stopPrank(); assertTrue(escrow.isTokenWhitelisted(token1)); assertTrue(escrow.isTokenWhitelisted(token2)); - assertEq(escrow.fee(), 750); + assertEq(escrow.feeOnClaim(), 750); assertEq(escrow.feeRecipient(), newRecipient); assertEq(escrow.signer(), newSignerAddr); assertEq(escrow.batchLimit(), 25); @@ -438,13 +438,13 @@ contract OnlyOwner_Test is Base_Test { // Old owner should not be able to make changes expectRevert("UNAUTHORIZED"); vm.prank(owner); - escrow.setFee(500); + escrow.setFeeOnClaim(500); // New owner should be able to make changes vm.prank(newOwner); - escrow.setFee(500); + escrow.setFeeOnClaim(500); - assertEq(escrow.fee(), 500); + assertEq(escrow.feeOnClaim(), 500); assertEq(escrow.owner(), newOwner); } @@ -463,7 +463,7 @@ contract OnlyOwner_Test is Base_Test { expectRevert("UNAUTHORIZED"); vm.prank(user); - escrow.setFee(500); + escrow.setFeeOnClaim(500); expectRevert("UNAUTHORIZED"); vm.prank(user); @@ -484,7 +484,8 @@ contract OnlyOwner_Test is Base_Test { /* -------------------------------------------------------------------------- */ event WhitelistedToken(address indexed token); - event FeeSet(uint256 oldFee, uint256 newFee); + event FeeOnClaimSet(uint256 oldFee, uint256 newFee); + event FeeOnFundSet(uint256 oldFee, uint256 newFee); event FeeRecipientSet(address indexed oldRecipient, address indexed newRecipient); event SignerSet(address indexed oldSigner, address indexed newSigner); event BatchLimitSet(uint256 newBatchLimit); @@ -493,36 +494,36 @@ contract OnlyOwner_Test is Base_Test { /* FEE SNAPSHOT INTERACTION TESTS */ /* -------------------------------------------------------------------------- */ - function test_setFee_doesNotAffectExistingDistributions() public { + function test_setFeeOnClaim_doesNotAffectExistingDistributions() public { // Create distributions with initial fee vm.prank(owner); - escrow.setFee(300); // 3% + escrow.setFeeOnClaim(300); // 3% // Setup repo and create distributions _setupRepoAndCreateDistributions(300); // This creates distributions with 3% fee // Change fee after distributions are created vm.prank(owner); - escrow.setFee(800); // 8% + escrow.setFeeOnClaim(800); // 8% // Check that global fee changed - assertEq(escrow.fee(), 800, "Global fee should have changed"); + assertEq(escrow.feeOnClaim(), 800, "Global fee should have changed"); // Check that existing distributions retain their original fee _verifyDistributionFeesUnchanged(300); // Verify they still have 3% fee } - function test_setFee_newDistributionsUseNewFee() public { + function test_setFeeOnClaim_newDistributionsUseNewFee() public { // Start with one fee vm.prank(owner); - escrow.setFee(200); // 2% + escrow.setFeeOnClaim(200); // 2% // Create first distribution _setupRepoAndCreateDistributions(200); // Change fee vm.prank(owner); - escrow.setFee(700); // 7% + escrow.setFeeOnClaim(700); // 7% // Create second distribution with new fee address recipient2 = makeAddr("recipient2"); @@ -537,7 +538,7 @@ contract OnlyOwner_Test is Base_Test { assertEq(dist2.fee, 700, "New distribution should use current fee"); } - function test_setFee_multipleChangesCreateHistoricalSnapshot() public { + function test_setFeeOnClaim_multipleChangesCreateHistoricalSnapshot() public { // Test that multiple fee changes create a historical record in distributions address recipient1 = makeAddr("recipient1"); address recipient2 = makeAddr("recipient2"); @@ -545,15 +546,15 @@ contract OnlyOwner_Test is Base_Test { // Create distributions with different fees over time vm.prank(owner); - escrow.setFee(100); // 1% + escrow.setFeeOnClaim(100); // 1% _createSingleDistribution(recipient1, 1000e18); vm.prank(owner); - escrow.setFee(500); // 5% + escrow.setFeeOnClaim(500); // 5% _createSingleDistribution(recipient2, 1000e18); vm.prank(owner); - escrow.setFee(900); // 9% + escrow.setFeeOnClaim(900); // 9% _createSingleDistribution(recipient3, 1000e18); // Verify each distribution preserved its creation-time fee @@ -566,23 +567,23 @@ contract OnlyOwner_Test is Base_Test { assertEq(dist3.fee, 900, "Third distribution should have 9% fee"); // Verify global fee is the latest - assertEq(escrow.fee(), 900, "Global fee should be latest value"); + assertEq(escrow.feeOnClaim(), 900, "Global fee should be latest value"); } - function test_setFee_zeroToNonZeroDoesNotAffectExisting() public { + function test_setFeeOnClaim_zeroToNonZeroDoesNotAffectExisting() public { // Start with zero fee vm.prank(owner); - escrow.setFee(0); // 0% + escrow.setFeeOnClaim(0); // 0% _setupRepoAndCreateDistributions(0); // Change to non-zero fee vm.prank(owner); - escrow.setFee(1000); // 10% + escrow.setFeeOnClaim(1000); // 10% // Existing distributions should still have 0% fee _verifyDistributionFeesUnchanged(0); - assertEq(escrow.fee(), 1000, "Global fee should be 10%"); + assertEq(escrow.feeOnClaim(), 1000, "Global fee should be 10%"); } // Helper functions for fee snapshot tests @@ -687,7 +688,7 @@ contract OnlyOwner_Test is Base_Test { } /// @dev Test fee setting with rapid changes to verify state consistency - function test_setFee_rapidChanges() public { + function test_setFeeOnClaim_rapidChanges() public { uint16[] memory feeRates = new uint16[](10); feeRates[0] = 0; feeRates[1] = 50; @@ -701,15 +702,15 @@ contract OnlyOwner_Test is Base_Test { feeRates[9] = 100; for (uint256 i = 0; i < feeRates.length; i++) { - uint256 previousFee = escrow.fee(); + uint256 previousFee = escrow.feeOnClaim(); vm.expectEmit(true, true, true, true); - emit FeeSet(previousFee, feeRates[i]); + emit FeeOnClaimSet(previousFee, feeRates[i]); vm.prank(owner); - escrow.setFee(feeRates[i]); + escrow.setFeeOnClaim(feeRates[i]); - assertEq(escrow.fee(), feeRates[i]); + assertEq(escrow.feeOnClaim(), feeRates[i]); } } @@ -900,11 +901,11 @@ contract OnlyOwner_Test is Base_Test { } /// @dev Test extreme fee rate changes and their mathematical consistency - function testFuzz_setFee_extremeRatesConsistency(uint16 feeRate) public { + function testFuzz_setFeeOnClaim_extremeRatesConsistency(uint16 feeRate) public { feeRate = uint16(bound(feeRate, 0, 1000)); // 0-10% vm.prank(owner); - escrow.setFee(feeRate); + escrow.setFeeOnClaim(feeRate); // Test that fee calculations remain mathematically sound uint256[] memory testAmounts = new uint256[](5); @@ -934,4 +935,202 @@ contract OnlyOwner_Test is Base_Test { } } } + + /* -------------------------------------------------------------------------- */ + /* SET FEE ON FUND TESTS */ + /* -------------------------------------------------------------------------- */ + + function test_setFeeOnFund_success() public { + uint256 newFee = 500; // 5% + assertEq(escrow.feeOnFund(), 0); // Initial fee from setup (should be 0) + + vm.prank(owner); + escrow.setFeeOnFund(newFee); + + assertEq(escrow.feeOnFund(), newFee); + } + + function test_setFeeOnFund_zeroFee() public { + // First set to non-zero + vm.prank(owner); + escrow.setFeeOnFund(500); + + // Then set back to zero + vm.prank(owner); + escrow.setFeeOnFund(0); + + assertEq(escrow.feeOnFund(), 0); + } + + function test_setFeeOnFund_maxFee() public { + uint256 maxFee = escrow.MAX_FEE(); // 10% + + vm.prank(owner); + escrow.setFeeOnFund(maxFee); + + assertEq(escrow.feeOnFund(), maxFee); + } + + function test_setFeeOnFund_revert_notOwner() public { + expectRevert("UNAUTHORIZED"); + vm.prank(unauthorized); + escrow.setFeeOnFund(500); + } + + function test_setFeeOnFund_revert_exceedsMaxFee() public { + uint256 invalidFee = escrow.MAX_FEE() + 1; + + expectRevert(Errors.INVALID_FEE); + vm.prank(owner); + escrow.setFeeOnFund(invalidFee); + } + + function test_setFeeOnFund_fuzz(uint256 fee) public { + vm.assume(fee <= escrow.MAX_FEE()); + + vm.prank(owner); + escrow.setFeeOnFund(fee); + + assertEq(escrow.feeOnFund(), fee); + } + + function test_setFeeOnFund_emitsEvent() public { + uint256 newFee = 500; // 5% + vm.expectEmit(true, true, true, true); + emit FeeOnFundSet(escrow.feeOnFund(), newFee); + vm.prank(owner); + escrow.setFeeOnFund(newFee); + } + + function test_setFeeOnFund_multipleChanges() public { + uint256[] memory feeRates = new uint256[](5); + feeRates[0] = 100; // 1% + feeRates[1] = 250; // 2.5% + feeRates[2] = 500; // 5% + feeRates[3] = 750; // 7.5% + feeRates[4] = 1000; // 10% + + for (uint i = 0; i < feeRates.length; i++) { + uint256 previousFee = escrow.feeOnFund(); + + vm.expectEmit(true, true, true, true); + emit FeeOnFundSet(previousFee, feeRates[i]); + + vm.prank(owner); + escrow.setFeeOnFund(feeRates[i]); + + assertEq(escrow.feeOnFund(), feeRates[i]); + } + } + + function test_setFeeOnFund_independentFromClaimFee() public { + // Set claim fee to one value + vm.prank(owner); + escrow.setFeeOnClaim(300); // 3% + + // Set fund fee to different value + vm.prank(owner); + escrow.setFeeOnFund(700); // 7% + + // Verify they are independent + assertEq(escrow.feeOnClaim(), 300); + assertEq(escrow.feeOnFund(), 700); + + // Change one, verify other is unchanged + vm.prank(owner); + escrow.setFeeOnClaim(100); + + assertEq(escrow.feeOnClaim(), 100); + assertEq(escrow.feeOnFund(), 700); // Should remain unchanged + } + + function test_setFeeOnFund_immediateEffect() public { + // First set a fee rate + vm.prank(owner); + escrow.setFeeOnFund(200); // 2% + + // Create token and fund repo to test immediate effect + MockERC20 testToken = new MockERC20("Test", "TEST", 18); + vm.prank(owner); + escrow.whitelistToken(address(testToken)); + + uint256 fundAmount = 1000e18; + address funder = makeAddr("funder"); + + testToken.mint(funder, fundAmount); + vm.prank(funder); + testToken.approve(address(escrow), fundAmount); + + // Fund with 2% fee + vm.prank(funder); + escrow.fundRepo(1, 1, testToken, fundAmount, ""); + + uint256 expectedNet1 = fundAmount - (fundAmount * 200) / 10_000; // 980e18 + assertEq(escrow.getAccountBalance(1, 1, address(testToken)), expectedNet1); + + // Change fee immediately + vm.prank(owner); + escrow.setFeeOnFund(800); // 8% + + // Fund again with new fee rate + testToken.mint(funder, fundAmount); + vm.prank(funder); + testToken.approve(address(escrow), fundAmount); + + vm.prank(funder); + escrow.fundRepo(1, 1, testToken, fundAmount, ""); + + uint256 expectedNet2 = fundAmount - (fundAmount * 800) / 10_000; // 920e18 + uint256 totalExpected = expectedNet1 + expectedNet2; + + assertEq(escrow.getAccountBalance(1, 1, address(testToken)), totalExpected); + } + + function test_setFeeOnFund_extremeValues() public { + // Test boundary values + uint256[] memory extremeFees = new uint256[](3); + extremeFees[0] = 0; // 0% + extremeFees[1] = 1; // 0.01% + extremeFees[2] = 1000; // 10% + + for (uint i = 0; i < extremeFees.length; i++) { + vm.prank(owner); + escrow.setFeeOnFund(extremeFees[i]); + assertEq(escrow.feeOnFund(), extremeFees[i]); + } + } + + function test_setFeeOnFund_gasEfficiency() public { + // Measure gas for setting fee + vm.prank(owner); + uint256 gasBefore = gasleft(); + escrow.setFeeOnFund(500); + uint256 gasUsed = gasBefore - gasleft(); + + // Should be relatively low gas (similar to setting a storage variable) + // This is more of a benchmark than a hard assertion + assertTrue(gasUsed < 50000, "Fee setting should be gas efficient"); + } + + function test_bothFees_fuzz_independentOperations(uint16 claimFee, uint16 fundFee) public { + vm.assume(claimFee <= 1000 && fundFee <= 1000); + + // Set both fees + vm.prank(owner); + escrow.setFeeOnClaim(claimFee); + + vm.prank(owner); + escrow.setFeeOnFund(fundFee); + + // Verify both are set correctly + assertEq(escrow.feeOnClaim(), claimFee); + assertEq(escrow.feeOnFund(), fundFee); + + // Change one, verify independence + vm.prank(owner); + escrow.setFeeOnClaim(0); + + assertEq(escrow.feeOnClaim(), 0); + assertEq(escrow.feeOnFund(), fundFee); // Should remain unchanged + } } \ No newline at end of file