From 808f0fcf570c4d5a525b7d77f3ddb7bc555133ca Mon Sep 17 00:00:00 2001 From: nourharidy Date: Sat, 6 Sep 2025 15:58:48 +0400 Subject: [PATCH 1/8] add src and tests --- .github/workflows/test.yml | 40 +++++++++ .gitignore | 14 +++ .gitmodules | 3 + README.md | 67 ++++++++++++++- foundry.lock | 8 ++ foundry.toml | 9 ++ lib/forge-std | 1 + script/Payroll.s.sol | 19 +++++ src/Payroll.sol | 83 ++++++++++++++++++ test/Payroll.t.sol | 170 +++++++++++++++++++++++++++++++++++++ test/utils/MockERC20.sol | 48 +++++++++++ 11 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 foundry.lock create mode 100644 foundry.toml create mode 160000 lib/forge-std create mode 100644 script/Payroll.s.sol create mode 100644 src/Payroll.sol create mode 100644 test/Payroll.t.sol create mode 100644 test/utils/MockERC20.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4481ec6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index d11894a..8817d6a 100644 --- a/README.md +++ b/README.md @@ -1 +1,66 @@ -# payroll \ No newline at end of file +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..5643642 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.10.0", + "rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824" + } + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..57da4fb --- /dev/null +++ b/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +[lint] +severity = ["high"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..8bbcf6e --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8bbcf6e3f8f62f419e5429a0bd89331c85c37824 diff --git a/script/Payroll.s.sol b/script/Payroll.s.sol new file mode 100644 index 0000000..bd2f34a --- /dev/null +++ b/script/Payroll.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {Payroll} from "../src/Payroll.sol"; + +contract CounterScript is Script { + Payroll public payroll; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + //payroll = new Payroll(); + + vm.stopBroadcast(); + } +} diff --git a/src/Payroll.sol b/src/Payroll.sol new file mode 100644 index 0000000..1bbcc0d --- /dev/null +++ b/src/Payroll.sol @@ -0,0 +1,83 @@ +pragma solidity 0.8.13; + +interface IERC20 { + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +contract Payroll { + + mapping(address => Recipient) public recipients; + mapping(address => uint256) public unclaimed; + + address public immutable treasuryAddress; + address public immutable governance; + IERC20 public immutable DOLA; + + uint256 public constant SECONDS_PER_YEAR = 365 days; + + struct Recipient { + uint256 lastClaim; + uint256 ratePerSecond; + uint256 endTime; + } + + event SetRecipient(address recipient, uint256 amount, uint256 endTime); + event RecipientRemoved(address recipient); + event AmountWithdrawn(address recipient, uint256 amount); + + constructor(address _treasuryAddress, address _governance, address _DOLA) { + treasuryAddress = _treasuryAddress; + governance = _governance; + DOLA = IERC20(_DOLA); + } + + function balanceOf(address _recipient) public view returns (uint256 bal) { + bal = unclaimed[_recipient]; + Recipient memory recipient = recipients[_recipient]; + if (recipient.endTime > block.timestamp) { + // recipient is still active + bal += recipient.ratePerSecond * (block.timestamp - recipient.lastClaim); + } else { + // recipient is no longer active + bal += recipient.ratePerSecond * (recipient.endTime - recipient.lastClaim); + } + } + + function updateRecipient(address recipient) internal { + unclaimed[recipient] = balanceOf(recipient); + recipients[recipient].lastClaim = block.timestamp; + } + + function setRecipient(address _recipient, uint256 _yearlyAmount, uint256 _endTime) external { + updateRecipient(_recipient); + require(msg.sender == governance, "DolaPayroll::setRecipient: only governance"); + require(_recipient != address(0), "DolaPayroll::setRecipient: zero address!"); + + // if endTime is in the past, set it to the current block timestamp to avoid underflow in balanceOf + if(_endTime < block.timestamp) { + _endTime = block.timestamp; + } + + recipients[_recipient] = Recipient({ + lastClaim: block.timestamp, + ratePerSecond: _yearlyAmount / SECONDS_PER_YEAR, + endTime: _endTime + }); + + emit SetRecipient(_recipient, _yearlyAmount, _endTime); + } + + /** + * @notice withdraw salary + */ + function withdraw() external { + updateRecipient(msg.sender); + + uint256 amount = unclaimed[msg.sender]; + unclaimed[msg.sender] = 0; + require(DOLA.transferFrom(treasuryAddress, msg.sender, amount), "DolaPayroll::withdraw: transfer failed"); + + emit AmountWithdrawn(msg.sender, amount); + } + +} \ No newline at end of file diff --git a/test/Payroll.t.sol b/test/Payroll.t.sol new file mode 100644 index 0000000..e4344b6 --- /dev/null +++ b/test/Payroll.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {Payroll} from "../src/Payroll.sol"; +import {MockERC20} from "./utils/MockERC20.sol"; + +contract PayrollTest is Test { + Payroll public payroll; + MockERC20 public dola; + + address public governance; + address public treasury; + address public alice; + address public bob; + + event SetRecipient(address recipient, uint256 amount, uint256 endTime); + event RecipientRemoved(address recipient); + event AmountWithdrawn(address recipient, uint256 amount); + + function setUp() public { + governance = makeAddr("governance"); + treasury = makeAddr("treasury"); + alice = makeAddr("alice"); + bob = makeAddr("bob"); + + dola = new MockERC20("DOLA", "DOLA"); + dola.mint(treasury, 1_000_000 ether); + + payroll = new Payroll(treasury, governance, address(dola)); + + vm.prank(treasury); + dola.approve(address(payroll), type(uint256).max); + } + + function test_constructor_sets_immutables() public { + assertEq(payroll.treasuryAddress(), treasury); + assertEq(payroll.governance(), governance); + assertEq(address(payroll.DOLA()), address(dola)); + } + + function test_setRecipient_only_governance() public { + uint256 endTime = block.timestamp + 10; + vm.prank(alice); + vm.expectRevert(bytes("DolaPayroll::setRecipient: only governance")); + payroll.setRecipient(alice, 100, endTime); + } + + function test_setRecipient_zero_address_reverts() public { + uint256 endTime = block.timestamp + 10; + vm.prank(governance); + vm.expectRevert(bytes("DolaPayroll::setRecipient: zero address!")); + payroll.setRecipient(address(0), 100, endTime); + } + + function test_setRecipient_sets_fields_and_emits_event() public { + uint256 t0 = 1_000_000; + vm.warp(t0); + + uint256 endTime = t0 + 10_000; + uint256 yearly = 365 days * 1_000; // ratePerSecond should be 1_000 + + vm.expectEmit(true, true, true, true); + emit SetRecipient(alice, yearly, endTime); + + vm.prank(governance); + payroll.setRecipient(alice, yearly, endTime); + + (uint256 lastClaim, uint256 ratePerSecond, uint256 end) = payroll.recipients(alice); + assertEq(lastClaim, t0); + assertEq(ratePerSecond, 1_000); + assertEq(end, endTime); + } + + function test_setRecipient_past_end_normalized_to_now() public { + uint256 t0 = 2_000_000; + vm.warp(t0); + + uint256 pastEnd = t0 - 1_234; + uint256 yearly = 365 days * 10; + + vm.expectEmit(true, true, true, true); + emit SetRecipient(alice, yearly, t0); + + vm.prank(governance); + payroll.setRecipient(alice, yearly, pastEnd); + + (, uint256 ratePerSecond, uint256 end) = payroll.recipients(alice); + assertEq(ratePerSecond, 10); + assertEq(end, t0); + } + + function test_balanceOf_during_active_period() public { + uint256 t0 = 3_000_000; + vm.warp(t0); + uint256 yearly = 365 days * 10; // rate 10/sec + + vm.prank(governance); + payroll.setRecipient(alice, yearly, t0 + 10_000); + + vm.warp(t0 + 1234); // 1234 seconds passed + uint256 bal = payroll.balanceOf(alice); + assertEq(bal, 10 * 1234); + } + + function test_balanceOf_after_end_time() public { + uint256 t0 = 4_000_000; + vm.warp(t0); + uint256 yearly = 365 days * 10; // rate 10/sec + + vm.prank(governance); + payroll.setRecipient(alice, yearly, t0 + 400); + + vm.warp(t0 + 1_000); // past end + uint256 bal = payroll.balanceOf(alice); + // accrues only until end (400 seconds) + assertEq(bal, 10 * 400); + } + + function test_withdraw_transfers_and_resets_unclaimed_and_emits_event() public { + uint256 t0 = 5_000_000; + vm.warp(t0); + uint256 yearly = 365 days * 20; // rate 20/sec + + vm.prank(governance); + payroll.setRecipient(alice, yearly, t0 + 10_000); + + vm.warp(t0 + 123); + uint256 expectedAmount = 20 * 123; + + uint256 treasuryBefore = dola.balanceOf(treasury); + uint256 aliceBefore = dola.balanceOf(alice); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit AmountWithdrawn(alice, expectedAmount); + payroll.withdraw(); + + uint256 treasuryAfter = dola.balanceOf(treasury); + uint256 aliceAfter = dola.balanceOf(alice); + + assertEq(treasuryAfter, treasuryBefore - expectedAmount); + assertEq(aliceAfter, aliceBefore + expectedAmount); + assertEq(payroll.unclaimed(alice), 0); + } + + function test_updateRecipient_accrues_prior_unclaimed_before_rate_change() public { + uint256 t0 = 6_000_000; + vm.warp(t0); + uint256 yearly1 = 365 days * 10; // 10/sec + uint256 yearly2 = 365 days * 20; // 20/sec + + vm.prank(governance); + payroll.setRecipient(alice, yearly1, t0 + 10_000); + + vm.warp(t0 + 100); + // Changing to a new rate; internal updateRecipient should accrue the first 100 * 10 + vm.prank(governance); + payroll.setRecipient(alice, yearly2, t0 + 20_000); + + // unclaimed should contain the accrued amount from the first schedule + assertEq(payroll.unclaimed(alice), 10 * 100); + + // New accrual from the second schedule after lastClaim reset at t0+100 + vm.warp(t0 + 150); + uint256 bal = payroll.balanceOf(alice); + // 100*10 (old) + 50*20 (new) + assertEq(bal, (10 * 100) + (20 * 50)); + } +} diff --git a/test/utils/MockERC20.sol b/test/utils/MockERC20.sol new file mode 100644 index 0000000..f882879 --- /dev/null +++ b/test/utils/MockERC20.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract MockERC20 { + string public name; + string public symbol; + uint8 public immutable decimals = 18; + + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + } + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + totalSupply += amount; + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + return true; + } + + function transfer(address to, uint256 amount) external returns (bool) { + require(balanceOf[msg.sender] >= amount, "insufficient balance"); + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + require(allowed >= amount, "insufficient allowance"); + require(balanceOf[from] >= amount, "insufficient balance"); + if (allowed != type(uint256).max) { + allowance[from][msg.sender] = allowed - amount; + } + balanceOf[from] -= amount; + balanceOf[to] += amount; + return true; + } +} + + From 58e729246b8e5ecc8394260b3446d9823d045f52 Mon Sep 17 00:00:00 2001 From: nourharidy Date: Sun, 21 Sep 2025 08:39:00 +0400 Subject: [PATCH 2/8] add withdraw uint amount --- src/Payroll.sol | 10 +++++----- test/Payroll.t.sol | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Payroll.sol b/src/Payroll.sol index 1bbcc0d..0ae3397 100644 --- a/src/Payroll.sol +++ b/src/Payroll.sol @@ -70,14 +70,14 @@ contract Payroll { /** * @notice withdraw salary */ - function withdraw() external { + function withdraw(uint256 amount) external { updateRecipient(msg.sender); - uint256 amount = unclaimed[msg.sender]; - unclaimed[msg.sender] = 0; - require(DOLA.transferFrom(treasuryAddress, msg.sender, amount), "DolaPayroll::withdraw: transfer failed"); + uint256 withdrawAmount = unclaimed[msg.sender] > amount ? amount : unclaimed[msg.sender]; + unclaimed[msg.sender] -= withdrawAmount; + require(DOLA.transferFrom(treasuryAddress, msg.sender, withdrawAmount), "DolaPayroll::withdraw: transfer failed"); - emit AmountWithdrawn(msg.sender, amount); + emit AmountWithdrawn(msg.sender, withdrawAmount); } } \ No newline at end of file diff --git a/test/Payroll.t.sol b/test/Payroll.t.sol index e4344b6..ed20c86 100644 --- a/test/Payroll.t.sol +++ b/test/Payroll.t.sol @@ -134,7 +134,7 @@ contract PayrollTest is Test { vm.prank(alice); vm.expectEmit(true, true, true, true); emit AmountWithdrawn(alice, expectedAmount); - payroll.withdraw(); + payroll.withdraw(expectedAmount); uint256 treasuryAfter = dola.balanceOf(treasury); uint256 aliceAfter = dola.balanceOf(alice); From 348efd0c478e3bc4cd64e7e94b2ec39bb9cc9eaa Mon Sep 17 00:00:00 2001 From: nourharidy Date: Tue, 23 Sep 2025 15:39:17 +0400 Subject: [PATCH 3/8] reproduce bug test --- test/Payroll.t.sol | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/Payroll.t.sol b/test/Payroll.t.sol index ed20c86..0e17ac7 100644 --- a/test/Payroll.t.sol +++ b/test/Payroll.t.sol @@ -167,4 +167,29 @@ contract PayrollTest is Test { // 100*10 (old) + 50*20 (new) assertEq(bal, (10 * 100) + (20 * 50)); } + + function test_rehire_after_expired_payroll() public { + uint256 startTime = 7_000_000; + vm.warp(startTime); + + uint256 salary = 365 days * 10; // 10 tokens per second + uint256 endTime = startTime + 400; // expires after 400 seconds + + // Set up payroll for alice + vm.prank(governance); + payroll.setRecipient(alice, salary, endTime); + + // Warp far past expiration + vm.warp(startTime + 1000); + + // User withdraws earned amount - this triggers updateRecipient + // setting lastClaim to current time (past endTime) + vm.prank(alice); + payroll.withdraw(4000); // full earned amount: 10 tokens/sec * 400 sec + + // Attempt to rehire alice - this should fail if bug exists + // because balanceOf will underflow (endTime - lastClaim where lastClaim > endTime) + vm.prank(governance); + payroll.setRecipient(alice, salary, startTime + 2000); + } } From 8b519197bb9ea72acf3610da73c8df011b6a9fb9 Mon Sep 17 00:00:00 2001 From: nourharidy Date: Tue, 23 Sep 2025 15:47:44 +0400 Subject: [PATCH 4/8] bug fix --- src/Payroll.sol | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Payroll.sol b/src/Payroll.sol index 0ae3397..61b7b8a 100644 --- a/src/Payroll.sol +++ b/src/Payroll.sol @@ -34,13 +34,9 @@ contract Payroll { function balanceOf(address _recipient) public view returns (uint256 bal) { bal = unclaimed[_recipient]; Recipient memory recipient = recipients[_recipient]; - if (recipient.endTime > block.timestamp) { - // recipient is still active - bal += recipient.ratePerSecond * (block.timestamp - recipient.lastClaim); - } else { - // recipient is no longer active - bal += recipient.ratePerSecond * (recipient.endTime - recipient.lastClaim); - } + uint256 accrualEnd = block.timestamp < recipient.endTime ? block.timestamp : recipient.endTime; + uint256 accrualStart = recipient.lastClaim < accrualEnd ? recipient.lastClaim : accrualEnd; + bal += recipient.ratePerSecond * (accrualEnd - accrualStart); } function updateRecipient(address recipient) internal { @@ -53,7 +49,7 @@ contract Payroll { require(msg.sender == governance, "DolaPayroll::setRecipient: only governance"); require(_recipient != address(0), "DolaPayroll::setRecipient: zero address!"); - // if endTime is in the past, set it to the current block timestamp to avoid underflow in balanceOf + // endTime cannot be in the past if(_endTime < block.timestamp) { _endTime = block.timestamp; } From f603bacdb66fb0ddc0d19a260960822fdc6310c3 Mon Sep 17 00:00:00 2001 From: nourharidy Date: Tue, 23 Sep 2025 15:48:05 +0400 Subject: [PATCH 5/8] remove unused event --- src/Payroll.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Payroll.sol b/src/Payroll.sol index 61b7b8a..f71a1a0 100644 --- a/src/Payroll.sol +++ b/src/Payroll.sol @@ -22,7 +22,6 @@ contract Payroll { } event SetRecipient(address recipient, uint256 amount, uint256 endTime); - event RecipientRemoved(address recipient); event AmountWithdrawn(address recipient, uint256 amount); constructor(address _treasuryAddress, address _governance, address _DOLA) { From 42f236e385c70212ad6b3c65ea38f9e43d299a8b Mon Sep 17 00:00:00 2001 From: nourharidy Date: Sat, 27 Sep 2025 12:46:06 +0400 Subject: [PATCH 6/8] rename dola to asset --- src/Payroll.sol | 12 ++++++------ test/Payroll.t.sol | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Payroll.sol b/src/Payroll.sol index f71a1a0..f113cfa 100644 --- a/src/Payroll.sol +++ b/src/Payroll.sol @@ -11,7 +11,7 @@ contract Payroll { address public immutable treasuryAddress; address public immutable governance; - IERC20 public immutable DOLA; + IERC20 public immutable asset; uint256 public constant SECONDS_PER_YEAR = 365 days; @@ -24,10 +24,10 @@ contract Payroll { event SetRecipient(address recipient, uint256 amount, uint256 endTime); event AmountWithdrawn(address recipient, uint256 amount); - constructor(address _treasuryAddress, address _governance, address _DOLA) { + constructor(address _treasuryAddress, address _governance, address _asset) { treasuryAddress = _treasuryAddress; governance = _governance; - DOLA = IERC20(_DOLA); + asset = IERC20(_asset); } function balanceOf(address _recipient) public view returns (uint256 bal) { @@ -45,8 +45,8 @@ contract Payroll { function setRecipient(address _recipient, uint256 _yearlyAmount, uint256 _endTime) external { updateRecipient(_recipient); - require(msg.sender == governance, "DolaPayroll::setRecipient: only governance"); - require(_recipient != address(0), "DolaPayroll::setRecipient: zero address!"); + require(msg.sender == governance, "Payroll::setRecipient: only governance"); + require(_recipient != address(0), "Payroll::setRecipient: zero address!"); // endTime cannot be in the past if(_endTime < block.timestamp) { @@ -70,7 +70,7 @@ contract Payroll { uint256 withdrawAmount = unclaimed[msg.sender] > amount ? amount : unclaimed[msg.sender]; unclaimed[msg.sender] -= withdrawAmount; - require(DOLA.transferFrom(treasuryAddress, msg.sender, withdrawAmount), "DolaPayroll::withdraw: transfer failed"); + require(asset.transferFrom(treasuryAddress, msg.sender, withdrawAmount), "Payroll::withdraw: transfer failed"); emit AmountWithdrawn(msg.sender, withdrawAmount); } diff --git a/test/Payroll.t.sol b/test/Payroll.t.sol index 0e17ac7..7e65dcc 100644 --- a/test/Payroll.t.sol +++ b/test/Payroll.t.sol @@ -36,20 +36,20 @@ contract PayrollTest is Test { function test_constructor_sets_immutables() public { assertEq(payroll.treasuryAddress(), treasury); assertEq(payroll.governance(), governance); - assertEq(address(payroll.DOLA()), address(dola)); + assertEq(address(payroll.asset()), address(dola)); } function test_setRecipient_only_governance() public { uint256 endTime = block.timestamp + 10; vm.prank(alice); - vm.expectRevert(bytes("DolaPayroll::setRecipient: only governance")); + vm.expectRevert(bytes("Payroll::setRecipient: only governance")); payroll.setRecipient(alice, 100, endTime); } function test_setRecipient_zero_address_reverts() public { uint256 endTime = block.timestamp + 10; vm.prank(governance); - vm.expectRevert(bytes("DolaPayroll::setRecipient: zero address!")); + vm.expectRevert(bytes("Payroll::setRecipient: zero address!")); payroll.setRecipient(address(0), 100, endTime); } From c2413eeeffe101fd3cb09b8af61c8c63b8ccf6be Mon Sep 17 00:00:00 2001 From: nourharidy Date: Fri, 3 Oct 2025 18:40:25 +0300 Subject: [PATCH 7/8] require 18 decimal tokens --- src/Payroll.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Payroll.sol b/src/Payroll.sol index f113cfa..e8a216c 100644 --- a/src/Payroll.sol +++ b/src/Payroll.sol @@ -2,8 +2,10 @@ pragma solidity 0.8.13; interface IERC20 { function transferFrom(address from, address to, uint256 amount) external returns (bool); + function decimals() external view returns (uint8); } +// WARNING: THIS CONTRACT IS NOT COMPATIBLE WITH NON-STANDARD ERC20 TOKENS (e.g. USDT) contract Payroll { mapping(address => Recipient) public recipients; @@ -25,6 +27,7 @@ contract Payroll { event AmountWithdrawn(address recipient, uint256 amount); constructor(address _treasuryAddress, address _governance, address _asset) { + require(IERC20(_asset).decimals() == 18, "Payroll::constructor: asset must have 18 decimals"); treasuryAddress = _treasuryAddress; governance = _governance; asset = IERC20(_asset); From 23c9556beb207da8dcf8fe9322baf5e2bd94cf73 Mon Sep 17 00:00:00 2001 From: nourharidy Date: Fri, 3 Oct 2025 18:41:03 +0300 Subject: [PATCH 8/8] index event recipients --- src/Payroll.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Payroll.sol b/src/Payroll.sol index e8a216c..b15a996 100644 --- a/src/Payroll.sol +++ b/src/Payroll.sol @@ -23,8 +23,8 @@ contract Payroll { uint256 endTime; } - event SetRecipient(address recipient, uint256 amount, uint256 endTime); - event AmountWithdrawn(address recipient, uint256 amount); + event SetRecipient(address indexed recipient, uint256 amount, uint256 endTime); + event AmountWithdrawn(address indexed recipient, uint256 amount); constructor(address _treasuryAddress, address _governance, address _asset) { require(IERC20(_asset).decimals() == 18, "Payroll::constructor: asset must have 18 decimals");