From e0ad884637ab52cdd5d5c97232f36108108e5e84 Mon Sep 17 00:00:00 2001 From: ququzone Date: Mon, 2 Sep 2024 15:19:05 +0800 Subject: [PATCH 1/3] add PrriodClaimVault --- contracts/PeriodClaimVault.sol | 124 +++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 contracts/PeriodClaimVault.sol diff --git a/contracts/PeriodClaimVault.sol b/contracts/PeriodClaimVault.sol new file mode 100644 index 0000000..9aaedc7 --- /dev/null +++ b/contracts/PeriodClaimVault.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +interface IioIDStore { + function projectDeviceContract(uint256 _projectId) external view returns (address); + function projectActivedAmount(uint256 _projectId) external view returns (uint256); +} + +contract PeriodClaimVault is OwnableUpgradeable { + event Donation(address indexed donor, uint256 amount); + event Withdraw(address indexed owner, address recipcient, uint256 amount); + event Initialize(uint256 period, uint256 rewardPerDevice); + event AddProject(uint256 projectId, address recipient, uint256 startTimestamp); + event RemoveProject(uint256 projectId); + event Claim(uint256 projectId, address recipient, uint256 startTimestamp, uint256 endTimestamp, uint256 rewards); + event ChangePeriod(uint256 batchSize); + event ChangeRewardPerDevice(uint256 rewardPerBlock); + event ChangeRecipient(address indexed admin, uint256 projectId, address recipient); + event SetInvalidDevice(uint256 projectId, uint256 amount); + + IioIDStore public ioIDStore; + uint256 public period; + uint256 public rewardPerDevice; + uint256 public projectNum; + mapping(uint256 => address) public projectRecipient; + mapping(uint256 => uint256) public projectInvalidDevice; + mapping(uint256 => uint256) public lastClaimedTimestamp; + + function initialize(address _ioIDStore, uint256 _rewardPerDevice) public initializer { + require(_ioIDStore != address(0), "zero address"); + require(_rewardPerDevice > 0, "invalid reward per device"); + + __Ownable_init_unchained(); + + ioIDStore = IioIDStore(_ioIDStore); + uint256 _period = 1 days; + period = _period; + rewardPerDevice = _rewardPerDevice; + + emit Initialize(_period, _rewardPerDevice); + } + + function addProject(uint256 _projectId, address _recipient, uint256 _startTimestamp) external onlyOwner { + require(_recipient != address(0), "zero address"); + require(_startTimestamp > block.number, "invalid start timestamp"); + require(ioIDStore.projectDeviceContract(_projectId) != address(0), "invalid project"); + + projectNum++; + projectRecipient[_projectId] = _recipient; + lastClaimedTimestamp[_projectId] = _startTimestamp; + emit AddProject(_projectId, _recipient, _startTimestamp); + } + + function removeProject(uint256 _projectId) external onlyOwner { + require(projectRecipient[_projectId] != address(0), "invalid project"); + + delete projectRecipient[_projectId]; + delete lastClaimedTimestamp[_projectId]; + emit RemoveProject(_projectId); + } + + function claim(uint256 _projectId) external returns (uint256) { + uint256 _lastClaimedTimestamp = lastClaimedTimestamp[_projectId]; + require(_lastClaimedTimestamp != 0, "invalid project"); + require(_lastClaimedTimestamp + period <= block.number, "claim too short"); + uint256 _claimablePeriods = (block.number - _lastClaimedTimestamp) / period; + uint256 _rewards = _claimablePeriods * + rewardPerDevice * + (ioIDStore.projectActivedAmount(_projectId) - projectInvalidDevice[_projectId]); + require(address(this).balance >= _rewards, "insufficient fund"); + lastClaimedTimestamp[_projectId] += (_claimablePeriods * period); + address _recipient = projectRecipient[_projectId]; + (bool success, ) = payable(_recipient).call{value: _rewards}(""); + require(success, "transfer rewards failed"); + emit Claim(_projectId, _recipient, _lastClaimedTimestamp, lastClaimedTimestamp[_projectId], _rewards); + return _rewards; + } + + function changePeriod(uint256 _period) external onlyOwner { + require(_period > 0, "invalid period"); + period = _period; + + emit ChangePeriod(_period); + } + + function changeRewardPerDevice(uint256 _rewardPerDevice) external onlyOwner { + require(_rewardPerDevice > 0, "invalid reward per device"); + rewardPerDevice = _rewardPerDevice; + + emit ChangeRewardPerDevice(_rewardPerDevice); + } + + function setInvalidDevice(uint256 _projectId, uint256 _amount) external onlyOwner { + require(_amount > 0, "zero amount"); + require(ioIDStore.projectActivedAmount(_projectId) >= _amount, "invalid project"); + + projectInvalidDevice[_projectId] = _amount; + emit SetInvalidDevice(_projectId, _amount); + } + + function changeRecipient(uint256 _projectId, address _recipient) external { + require(_recipient != address(0), "zero address"); + require(msg.sender == owner() || msg.sender == projectRecipient[_projectId], "invalid admin"); + + projectRecipient[_projectId] = _recipient; + emit ChangeRecipient(msg.sender, _projectId, _recipient); + } + + receive() external payable { + emit Donation(msg.sender, msg.value); + } + + function donate() external payable { + emit Donation(msg.sender, msg.value); + } + + function withdraw(address payable _recipcient, uint256 _amount) external onlyOwner { + (bool success, ) = payable(_recipcient).call{value: _amount}(""); + require(success, "withdraw token failed"); + emit Withdraw(msg.sender, _recipcient, _amount); + } +} From dc1e9b7599111f320344246367d184825ef319f0 Mon Sep 17 00:00:00 2001 From: ququzone Date: Mon, 2 Sep 2024 16:20:13 +0800 Subject: [PATCH 2/3] change block number to block timestamp --- contracts/PeriodClaimVault.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/PeriodClaimVault.sol b/contracts/PeriodClaimVault.sol index 9aaedc7..cf79c18 100644 --- a/contracts/PeriodClaimVault.sol +++ b/contracts/PeriodClaimVault.sol @@ -16,7 +16,7 @@ contract PeriodClaimVault is OwnableUpgradeable { event RemoveProject(uint256 projectId); event Claim(uint256 projectId, address recipient, uint256 startTimestamp, uint256 endTimestamp, uint256 rewards); event ChangePeriod(uint256 batchSize); - event ChangeRewardPerDevice(uint256 rewardPerBlock); + event ChangeRewardPerDevice(uint256 rewardPerDevice); event ChangeRecipient(address indexed admin, uint256 projectId, address recipient); event SetInvalidDevice(uint256 projectId, uint256 amount); @@ -44,7 +44,7 @@ contract PeriodClaimVault is OwnableUpgradeable { function addProject(uint256 _projectId, address _recipient, uint256 _startTimestamp) external onlyOwner { require(_recipient != address(0), "zero address"); - require(_startTimestamp > block.number, "invalid start timestamp"); + require(_startTimestamp > block.timestamp, "invalid start timestamp"); require(ioIDStore.projectDeviceContract(_projectId) != address(0), "invalid project"); projectNum++; @@ -64,8 +64,8 @@ contract PeriodClaimVault is OwnableUpgradeable { function claim(uint256 _projectId) external returns (uint256) { uint256 _lastClaimedTimestamp = lastClaimedTimestamp[_projectId]; require(_lastClaimedTimestamp != 0, "invalid project"); - require(_lastClaimedTimestamp + period <= block.number, "claim too short"); - uint256 _claimablePeriods = (block.number - _lastClaimedTimestamp) / period; + require(_lastClaimedTimestamp + period <= block.timestamp, "claim too short"); + uint256 _claimablePeriods = (block.timestamp - _lastClaimedTimestamp) / period; uint256 _rewards = _claimablePeriods * rewardPerDevice * (ioIDStore.projectActivedAmount(_projectId) - projectInvalidDevice[_projectId]); From c175be015804a42530ecc902e3c5802ecea73a5b Mon Sep 17 00:00:00 2001 From: ququzone Date: Mon, 2 Sep 2024 19:18:05 +0800 Subject: [PATCH 3/3] add PrriodClaimVault tests --- contracts/BatchClaimVault.sol | 2 + contracts/PeriodClaimVault.sol | 3 +- contracts/test/TestIoIDStore.sol | 15 +++++++ test/TestPeriodClaimVault.ts | 77 ++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 contracts/test/TestIoIDStore.sol create mode 100644 test/TestPeriodClaimVault.ts diff --git a/contracts/BatchClaimVault.sol b/contracts/BatchClaimVault.sol index ddde7d9..c8f94a5 100644 --- a/contracts/BatchClaimVault.sol +++ b/contracts/BatchClaimVault.sol @@ -34,6 +34,7 @@ contract BatchClaimVault is OwnableUpgradeable { function addProject(uint256 _projectId, address _recipient, uint256 _startBlock) external onlyOwner { require(_recipient != address(0), "zero address"); require(_startBlock > block.number, "invalid start block"); + require(projectRecipient[_projectId] == address(0), "already added"); projectNum++; projectRecipient[_projectId] = _recipient; @@ -46,6 +47,7 @@ contract BatchClaimVault is OwnableUpgradeable { delete projectRecipient[_projectId]; delete lastClaimedBlock[_projectId]; + projectNum--; emit RemoveProject(_projectId); } diff --git a/contracts/PeriodClaimVault.sol b/contracts/PeriodClaimVault.sol index cf79c18..41b8ee5 100644 --- a/contracts/PeriodClaimVault.sol +++ b/contracts/PeriodClaimVault.sol @@ -45,6 +45,7 @@ contract PeriodClaimVault is OwnableUpgradeable { function addProject(uint256 _projectId, address _recipient, uint256 _startTimestamp) external onlyOwner { require(_recipient != address(0), "zero address"); require(_startTimestamp > block.timestamp, "invalid start timestamp"); + require(projectRecipient[_projectId] == address(0), "already added"); require(ioIDStore.projectDeviceContract(_projectId) != address(0), "invalid project"); projectNum++; @@ -58,6 +59,7 @@ contract PeriodClaimVault is OwnableUpgradeable { delete projectRecipient[_projectId]; delete lastClaimedTimestamp[_projectId]; + projectNum--; emit RemoveProject(_projectId); } @@ -93,7 +95,6 @@ contract PeriodClaimVault is OwnableUpgradeable { } function setInvalidDevice(uint256 _projectId, uint256 _amount) external onlyOwner { - require(_amount > 0, "zero amount"); require(ioIDStore.projectActivedAmount(_projectId) >= _amount, "invalid project"); projectInvalidDevice[_projectId] = _amount; diff --git a/contracts/test/TestIoIDStore.sol b/contracts/test/TestIoIDStore.sol new file mode 100644 index 0000000..f249eb9 --- /dev/null +++ b/contracts/test/TestIoIDStore.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract TestIoIDStore { + mapping(uint256 => address) public projectDeviceContract; + mapping(uint256 => uint256) public projectActivedAmount; + + function setProjectDeviceContract(uint256 _projectId, address _contract) external { + projectDeviceContract[_projectId] = _contract; + } + + function setProjectActivedAmount(uint256 _projectId, uint256 _amount) external { + projectActivedAmount[_projectId] = _amount; + } +} diff --git a/test/TestPeriodClaimVault.ts b/test/TestPeriodClaimVault.ts new file mode 100644 index 0000000..a607733 --- /dev/null +++ b/test/TestPeriodClaimVault.ts @@ -0,0 +1,77 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers'; + +describe('PeriodClaimVault tests', function () { + it('claim', async function () { + const [owner, fakeRecipient] = await ethers.getSigners(); + + const ioIDStore = await ethers.deployContract('TestIoIDStore'); + const vault = await ethers.deployContract('PeriodClaimVault'); + await vault.connect(owner).initialize(ioIDStore.target, ethers.parseEther('0.1')); + + expect(await vault.period()).to.equal(86400); + expect(await vault.rewardPerDevice()).to.equal(ethers.parseEther('0.1')); + await expect(vault.connect(fakeRecipient).changePeriod(10)).to.revertedWith('Ownable: caller is not the owner'); + await expect(vault.connect(fakeRecipient).changeRewardPerDevice(10)).to.revertedWith( + 'Ownable: caller is not the owner', + ); + await expect(vault.connect(fakeRecipient).changeRecipient(1, fakeRecipient.address)).to.revertedWith( + 'invalid admin', + ); + + await expect(vault.setInvalidDevice(1, 10)).to.revertedWith('invalid project'); + expect(await vault.projectInvalidDevice(1)).to.be.equals(0); + await ioIDStore.setProjectActivedAmount(1, 5); + await ioIDStore.setProjectDeviceContract(1, '0x0000000000000000000000000000000000000001'); + await expect(vault.setInvalidDevice(1, 10)).to.revertedWith('invalid project'); + expect(await vault.projectInvalidDevice(1)).to.be.equals(0); + await vault.setInvalidDevice(1, 2); + expect(await vault.projectInvalidDevice(1)).to.be.equals(2); + await vault.setInvalidDevice(1, 0); + expect(await vault.projectInvalidDevice(1)).to.be.equals(0); + + const latestBlock = await ethers.provider.getBlock('latest'); + const startTimestamp = latestBlock!.timestamp + 2000; + + const projectId = 1; + const projectRecipient = '0x0000000000000000000000000000000000000100'; + await expect(vault.addProject(2, projectRecipient, startTimestamp)).to.revertedWith('invalid project'); + await vault.addProject(projectId, projectRecipient, startTimestamp); + await expect(vault.addProject(projectId, projectRecipient, startTimestamp)).to.revertedWith('already added'); + + expect(await vault.lastClaimedTimestamp(projectId)).to.be.equals(startTimestamp); + await expect(vault.claim(projectId)).to.revertedWith('claim too short'); + await time.increaseTo(startTimestamp + 86000); + await expect(vault.claim(projectId)).to.revertedWith('claim too short'); + await time.increaseTo(startTimestamp + 86400); + + await vault.donate({ value: ethers.parseEther('100') }); + await vault.claim(projectId); + expect(await ethers.provider.getBalance(projectRecipient)).to.be.equals( + (await ioIDStore.projectActivedAmount(projectId)) * (await vault.rewardPerDevice()), + ); + await expect(vault.claim(projectId)).to.revertedWith('claim too short'); + + await vault.setInvalidDevice(projectId, 2); + await expect(vault.claim(projectId)).to.revertedWith('claim too short'); + await time.increaseTo(startTimestamp + 86400 * 2 + 5000); + let balanceRecipient = await ethers.provider.getBalance(projectRecipient); + await vault.claim(projectId); + expect((await ethers.provider.getBalance(projectRecipient)) - balanceRecipient).to.be.equals( + ((await ioIDStore.projectActivedAmount(projectId)) - (await vault.projectInvalidDevice(projectId))) * + (await vault.rewardPerDevice()), + ); + + await time.increaseTo(startTimestamp + 86400 * 5 + 50000); + await vault.connect(fakeRecipient).claim(projectId); + expect((await ethers.provider.getBalance(projectRecipient)) - balanceRecipient).to.be.equals( + ((await ioIDStore.projectActivedAmount(projectId)) - (await vault.projectInvalidDevice(projectId))) * + (await vault.rewardPerDevice()) * + BigInt(4), + ); + + await vault.removeProject(projectId); + await expect(vault.claim(projectId)).to.revertedWith('invalid project'); + }); +});