diff --git a/contracts/RewardsBooster.sol b/contracts/RewardsBooster.sol index 2b3431d0..bf5b2bca 100644 --- a/contracts/RewardsBooster.sol +++ b/contracts/RewardsBooster.sol @@ -1018,6 +1018,7 @@ contract RewardsBooster is Initializable, OwnableUpgradeable, IRewardsBooster, S runnerDeplReward.accRewardsPerToken = accRewardsPerAllocatedToken; uint256 burnt; + sa.syncOverflowStatus(_runner); uint256 totalOverflowTime = sa.overAllocationTime(_runner); (reward, burnt) = _fixRewardsWithMissedLaborAndOverflow( reward, @@ -1078,6 +1079,7 @@ contract RewardsBooster is Initializable, OwnableUpgradeable, IRewardsBooster, S runnerDeplReward.accRewardsPerToken = accRewardsPerAllocatedToken; uint256 burnt; + sa.syncOverflowStatus(_runner); uint256 totalOverflowTime = sa.overAllocationTime(_runner); (reward, burnt) = _fixRewardsWithMissedLaborAndOverflow( reward, diff --git a/contracts/StakingAllocation.sol b/contracts/StakingAllocation.sol index 3f53cc04..3fcc8900 100644 --- a/contracts/StakingAllocation.sol +++ b/contracts/StakingAllocation.sol @@ -80,10 +80,7 @@ contract StakingAllocation is IStakingAllocation, Initializable, OwnableUpgradea .getEffectiveTotalStake(_runner); if (ia.overflowAt == 0 && ia.total < ia.used) { - // new overflow - emit OverAllocationStarted(_runner, block.timestamp); - - ia.overflowAt = block.timestamp; + _startOverAllocation(ia, _runner); } else if (ia.overflowAt != 0 && ia.total >= ia.used) { // recover from overflow emit OverAllocationEnded(_runner, block.timestamp, block.timestamp - ia.overflowAt); @@ -97,7 +94,7 @@ contract StakingAllocation is IStakingAllocation, Initializable, OwnableUpgradea require(_isAuth(_runner), 'SAL02'); require( IProjectRegistry(settings.getContractAddress(SQContracts.ProjectRegistry)) - .isServiceAvailable(_deployment, _runner), + .isServiceAvailable(_deployment, _runner), 'SAL05' ); @@ -114,10 +111,17 @@ contract StakingAllocation is IStakingAllocation, Initializable, OwnableUpgradea _removeAllocation(_deployment, _runner, _amount); } - function moveAllocation(bytes32 _deploymentFrom, bytes32 _deploymentTo, address _runner, uint256 _amount) external { + function moveAllocation( + bytes32 _deploymentFrom, + bytes32 _deploymentTo, + address _runner, + uint256 _amount + ) external { require(_isAuth(_runner), 'SAL02'); require(allocatedTokens[_runner][_deploymentFrom] >= _amount, 'SAL04'); require(_deploymentFrom != _deploymentTo, 'SAL07'); + RunnerAllocation storage ia = _runnerAllocations[_runner]; + require(ia.total >= ia.used, 'SAL03'); _removeAllocation(_deploymentFrom, _runner, _amount); _addAllocation(_deploymentTo, _runner, _amount); @@ -176,12 +180,19 @@ contract StakingAllocation is IStakingAllocation, Initializable, OwnableUpgradea return _runnerAllocations[_runner]; } + function syncOverflowStatus(address _runner) external override { + _syncOverflowStatus(_runner); + } + /** * @notice this returns the accumulated overflowTime of given runner */ function overAllocationTime(address _runner) external view returns (uint256) { RunnerAllocation memory ia = _runnerAllocations[_runner]; if (ia.total < ia.used) { + if (ia.overflowAt == 0) { + return ia.overflowTime; + } return ia.overflowTime + block.timestamp - ia.overflowAt; } else { return ia.overflowTime; @@ -199,4 +210,16 @@ contract StakingAllocation is IStakingAllocation, Initializable, OwnableUpgradea address controller = indexerRegistry.getController(_runner); return msg.sender == _runner || msg.sender == controller; } + + function _syncOverflowStatus(address _runner) private { + RunnerAllocation storage ia = _runnerAllocations[_runner]; + if (ia.overflowAt == 0 && ia.total < ia.used) { + _startOverAllocation(ia, _runner); + } + } + + function _startOverAllocation(RunnerAllocation storage ia, address _runner) private { + emit OverAllocationStarted(_runner, block.timestamp); + ia.overflowAt = block.timestamp; + } } diff --git a/contracts/interfaces/IStakingAllocation.sol b/contracts/interfaces/IStakingAllocation.sol index 7931f4ea..c1df6615 100644 --- a/contracts/interfaces/IStakingAllocation.sol +++ b/contracts/interfaces/IStakingAllocation.sol @@ -16,6 +16,8 @@ interface IStakingAllocation { function allocatedTokens(address _runner, bytes32 _deployment) external view returns (uint256); + function syncOverflowStatus(address _runner) external; + function runnerAllocation(address _runner) external view returns (RunnerAllocation memory); function overAllocationTime(address _runner) external view returns (uint256); diff --git a/publish/ABI/StakingAllocation.json b/publish/ABI/StakingAllocation.json index cc157b6b..60c9c3ee 100644 --- a/publish/ABI/StakingAllocation.json +++ b/publish/ABI/StakingAllocation.json @@ -411,6 +411,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_runner", + "type": "address" + } + ], + "name": "syncOverflowStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/test/StakingAllocation.test.ts b/test/StakingAllocation.test.ts index aad850ca..99958db8 100644 --- a/test/StakingAllocation.test.ts +++ b/test/StakingAllocation.test.ts @@ -242,7 +242,6 @@ describe('StakingAllocation Contract', () => { expect(da0).to.eq(etherParse('14000')); expect(da1).to.eq(etherParse('1000')); - await expect( stakingAllocation.connect(runner0).addAllocation(deploymentIds[1], runner0.address, etherParse('5001')) ).to.be.revertedWith('SAL03'); @@ -257,7 +256,6 @@ describe('StakingAllocation Contract', () => { await checkAllocation(runner1, etherParse('9000'), etherParse('10000'), true, false); await timeTravel(10); - await stakingAllocation .connect(runner1) .removeAllocation(deploymentIds[0], runner1.address, etherParse('500')); @@ -268,14 +266,17 @@ describe('StakingAllocation Contract', () => { await checkAllocation(runner1, etherParse('9000'), etherParse('9000'), false, true); await expect( - stakingAllocation.connect(runner1).moveAllocation(deploymentIds[0], deploymentIds[0], runner1.address, etherParse('1000')) + stakingAllocation + .connect(runner1) + .moveAllocation(deploymentIds[0], deploymentIds[0], runner1.address, etherParse('1000')) ).to.revertedWith('SAL07'); await expect( - stakingAllocation.connect(runner1).moveAllocation(deploymentIds[0], deploymentIds[1], runner1.address, etherParse('1199000')) + stakingAllocation + .connect(runner1) + .moveAllocation(deploymentIds[0], deploymentIds[1], runner1.address, etherParse('1199000')) ).to.revertedWith('SAL04'); - await stakingAllocation .connect(runner1) .moveAllocation(deploymentIds[0], deploymentIds[1], runner1.address, etherParse('1000')); @@ -292,10 +293,7 @@ describe('StakingAllocation Contract', () => { expect(await stakingAllocation.allocatedTokens(runner1.address, deploymentIds[0])).to.eq( etherParse('9000') ); - expect(await stakingAllocation.allocatedTokens(runner1.address, deploymentIds[1])).to.eq( - etherParse('0') - ); - + expect(await stakingAllocation.allocatedTokens(runner1.address, deploymentIds[1])).to.eq(etherParse('0')); }); it('add allocation to a stopped project', async () => { @@ -310,7 +308,6 @@ describe('StakingAllocation Contract', () => { await stakingAllocation.connect(runner0).addAllocation(deploymentId0, runner0.address, etherParse('5000')); await checkAllocation(runner0, etherParse('10000'), etherParse('5000'), false, false); - }); it('over-allocate and recover', async () => { @@ -351,6 +348,21 @@ describe('StakingAllocation Contract', () => { ).to.revertedWith('SAL03'); }); + it('blocks move allocation when runner is over-allocated', async () => { + await stakingAllocation + .connect(runner0) + .addAllocation(deploymentIds[0], runner0.address, etherParse('10000')); + + await stakingManager.connect(runner0).unstake(runner0.address, etherParse('1000')); + await applyStaking(runner0, runner0); + + await expect( + stakingAllocation + .connect(runner0) + .moveAllocation(deploymentIds[0], deploymentIds[1], runner0.address, etherParse('1000')) + ).to.be.revertedWith('SAL03'); + }); + it('remove allocation when stop service', async () => { await checkAllocation(runner0, etherParse('10000'), 0, false, false); await stakingManager.connect(runner0).stake(runner0.address, etherParse('10000'));