diff --git a/contracts/Dispenser.sol b/contracts/Dispenser.sol index 68c50102..6b10797e 100644 --- a/contracts/Dispenser.sol +++ b/contracts/Dispenser.sol @@ -1226,7 +1226,7 @@ contract Dispenser { _locked = 1; } - /// @dev Syncs the withheld amount according to the data received from L2. + /// @dev Syncs the withheld token amount according to the data received from L2. /// @notice Only a corresponding chain Id deposit processor is able to communicate the withheld amount data. /// Note that by design only a normalized withheld amount is delivered from L2. /// @param chainId L2 chain Id the withheld amount data is communicated from. diff --git a/contracts/staking/DefaultTargetDispenserL2.sol b/contracts/staking/DefaultTargetDispenserL2.sol index dbace6b3..52491d19 100644 --- a/contracts/staking/DefaultTargetDispenserL2.sol +++ b/contracts/staking/DefaultTargetDispenserL2.sol @@ -337,7 +337,7 @@ abstract contract DefaultTargetDispenserL2 is IBridgeErrors { /// @dev Syncs withheld token amount with L1. /// @param bridgePayload Payload data for the bridge relayer. - function syncWithheldTokens(bytes memory bridgePayload) external payable { + function syncWithheldAmount(bytes memory bridgePayload) external payable { // Reentrancy guard if (_locked > 1) { revert ReentrancyGuard(); diff --git a/docs/StakingSmartContracts.pdf b/docs/StakingSmartContracts.pdf index 14e83197..fdb2f966 100644 Binary files a/docs/StakingSmartContracts.pdf and b/docs/StakingSmartContracts.pdf differ diff --git a/test/DispenserStakingIncentives.js b/test/DispenserStakingIncentives.js index 0074a0c2..150bff4d 100644 --- a/test/DispenserStakingIncentives.js +++ b/test/DispenserStakingIncentives.js @@ -434,8 +434,6 @@ describe("DispenserStakingIncentives", async () => { // Set staking fraction to 100% await tokenomics.changeIncentiveFractions(0, 0, 0, 0, 0, 100); - // Changing staking parameters - await tokenomics.changeStakingParams(50, 10); // Checkpoint to apply changes await helpers.time.increase(epochLen); @@ -728,6 +726,44 @@ describe("DispenserStakingIncentives", async () => { await snapshot.restore(); }); + it("Should fail when trying to claim staking incentives for a retainer", async () => { + // Take a snapshot of the current state of the blockchain + const snapshot = await helpers.takeSnapshot(); + + // Set staking fraction to 100% + await tokenomics.changeIncentiveFractions(0, 0, 0, 0, 0, 100); + + // Checkpoint to apply changes + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // Unpause the dispenser + await dispenser.setPauseState(0); + + // Add deployer as a retainer nominee + await vw.addNominee(convertBytes32ToAddress(retainer), chainId); + + // Vote for the retainer + await vw.setNomineeRelativeWeight(convertBytes32ToAddress(retainer), chainId, defaultWeight); + + // Checkpoint to get to the next epoch + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // Try to claim incentives for the retainer + await expect( + dispenser.claimStakingIncentives(numClaimedEpochs, chainId, retainer, bridgePayload) + ).to.be.revertedWithCustomError(dispenser, "WrongAccount"); + + // Try to claim incentives for the retainer in the batch + await expect( + dispenser.claimStakingIncentivesBatch(numClaimedEpochs, [chainId], [[retainer]], [bridgePayload], [0]) + ).to.be.revertedWithCustomError(dispenser, "WrongAccount"); + + // Restore to the state of the snapshot + await snapshot.restore(); + }); + it("Sync withheld amount maintenance (DAO)", async () => { // Take a snapshot of the current state of the blockchain const snapshot = await helpers.takeSnapshot(); @@ -814,7 +850,15 @@ describe("DispenserStakingIncentives", async () => { // Claiming is not possible as it's the epoch after the staking contract was removed from nominees await expect( - dispenser.claimStakingIncentives(numClaimedEpochs, chainId, stakingTarget, bridgePayload) + dispenser.claimStakingIncentives(numClaimedEpochs + 1, chainId, stakingTarget, bridgePayload) + ).to.be.revertedWithCustomError(dispenser, "Overflow"); + + // Wait for one more epoch and try again + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + await expect( + dispenser.claimStakingIncentives(numClaimedEpochs + 1, chainId, stakingTarget, bridgePayload) ).to.be.revertedWithCustomError(dispenser, "Overflow"); // Restore to the state of the snapshot @@ -865,7 +909,7 @@ describe("DispenserStakingIncentives", async () => { ).to.be.revertedWithCustomError(dispenser, "DepositProcessorOnly"); // Sync back the withheld amount - await gnosisTargetDispenserL2.syncWithheldTokens(gnosisBridgePayload); + await gnosisTargetDispenserL2.syncWithheldAmount(gnosisBridgePayload); // Add a valid staking target nominee await vw.addNominee(stakingInstance.address, gnosisChainId); @@ -899,6 +943,90 @@ describe("DispenserStakingIncentives", async () => { await snapshot.restore(); }); + it("Should not claim anything is staking fraction is zero", async () => { + // Take a snapshot of the current state of the blockchain + const snapshot = await helpers.takeSnapshot(); + + // Set all fractions to zero + await tokenomics.changeIncentiveFractions(0, 0, 0, 0, 0, 0); + + // Checkpoint to apply changes + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // Unpause the dispenser + await dispenser.setPauseState(0); + + // Add a non-whitelisted staking instance as a nominee + await vw.addNominee(stakingInstance.address, chainId); + + const stakingTarget = convertAddressToBytes32(stakingInstance.address); + + // Vote for the nominee + await vw.setNomineeRelativeWeight(stakingInstance.address, chainId, defaultWeight); + + // Checkpoint to account for weights + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // Get the staking inflation for the previous epoch + const lastPoint = await tokenomics.epochCounter() - 1; + // Get the staking point of the last epoch + let sp = await tokenomics.mapEpochStakingPoints(lastPoint); + const sIncentive = sp.stakingIncentive; + + // Claim with withheld amount being accounted for + await dispenser.claimStakingIncentives(numClaimedEpochs, chainId, stakingTarget, bridgePayload); + + // Check that no OLAS has been distributed + expect(await olas.balanceOf(stakingInstance.address)).to.equal(0); + + // All the staking incentives must be returned into current epoch + sp = await tokenomics.mapEpochStakingPoints(lastPoint + 1); + expect(sp.stakingIncentive).to.equal(sIncentive); + + // Restore to the state of the snapshot + await snapshot.restore(); + }); + + it("Return all the staking inflation if there is no nominee", async () => { + // Take a snapshot of the current state of the blockchain + const snapshot = await helpers.takeSnapshot(); + + // Set staking fraction to 100% + await tokenomics.changeIncentiveFractions(0, 0, 0, 0, 0, 100); + // Changing staking parameters + await tokenomics.changeStakingParams(100, 10); + + // Checkpoint to apply changes + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // Unpause the dispenser + await dispenser.setPauseState(0); + + // Add a non-whitelisted staking instance as a nominee + await vw.addNominee(stakingInstance.address, chainId); + + const stakingTarget = convertAddressToBytes32(stakingInstance.address); + + // Vote for the nominee + await vw.setNomineeRelativeWeight(stakingInstance.address, chainId, 0); + + // Checkpoint to account for weights + await helpers.time.increase(epochLen); + await tokenomics.checkpoint(); + + // Claim with withheld amount being accounted for + await dispenser.claimStakingIncentives(numClaimedEpochs, chainId, stakingTarget, bridgePayload); + + // Check that no OLAS has been distributed + expect(await olas.balanceOf(stakingInstance.address)).to.equal(0); + + // Restore to the state of the snapshot + await snapshot.restore(); + }); + it("Claim staking incentives for a single nominee with bridging decimals lower than 18", async () => { // Take a snapshot of the current state of the blockchain const snapshot = await helpers.takeSnapshot(); @@ -986,7 +1114,7 @@ describe("DispenserStakingIncentives", async () => { expect(await gnosisTargetDispenserL2.withheldAmount()).to.gt(0); // Sync back the withheld amount - await gnosisTargetDispenserL2.syncWithheldTokens(gnosisBridgePayload); + await gnosisTargetDispenserL2.syncWithheldAmount(gnosisBridgePayload); // Get another staking instance const MockStakingProxy = await ethers.getContractFactory("MockStakingProxy"); diff --git a/test/StakingBridging.js b/test/StakingBridging.js index 324c9bc7..d2bb0ff1 100644 --- a/test/StakingBridging.js +++ b/test/StakingBridging.js @@ -402,11 +402,11 @@ describe("StakingBridging", async () => { expect(Number(withheldAmount)).to.equal(stakingIncentive); // Send withheld amount from L2 to L1 - await arbitrumTargetDispenserL2.syncWithheldTokens("0x"); + await arbitrumTargetDispenserL2.syncWithheldAmount("0x"); // Try to send withheld amount from L2 to L1 when there is none await expect( - arbitrumTargetDispenserL2.syncWithheldTokens("0x") + arbitrumTargetDispenserL2.syncWithheldAmount("0x") ).to.be.revertedWithCustomError(arbitrumDepositProcessorL1, "ZeroValue"); // Get staking batch hash @@ -653,7 +653,7 @@ describe("StakingBridging", async () => { // Trying to sync withheld tokens when paused await expect( - gnosisTargetDispenserL2.syncWithheldTokens("0x") + gnosisTargetDispenserL2.syncWithheldAmount("0x") ).to.be.revertedWithCustomError(gnosisTargetDispenserL2, "Paused"); // Unpause and send withheld amount from L2 to L1 @@ -661,14 +661,14 @@ describe("StakingBridging", async () => { // Send withheld token info from L2 to L1 when the gas is going to be adjusted from zero let bridgePayload = ethers.utils.defaultAbiCoder.encode(["uint256"], [0]); - await gnosisTargetDispenserL2.syncWithheldTokens(bridgePayload); + await gnosisTargetDispenserL2.syncWithheldAmount(bridgePayload); // Send a message on L2 with funds for a wrong address await dispenser.mintAndSend(gnosisDepositProcessorL1.address, deployer.address, stakingIncentive, "0x", stakingIncentive); // Send withheld token info from L2 to L1 when the gas is going to be adjusted without any payload - await gnosisTargetDispenserL2.syncWithheldTokens("0x"); + await gnosisTargetDispenserL2.syncWithheldAmount("0x"); // Send a message on L2 with funds for a wrong address await dispenser.mintAndSend(gnosisDepositProcessorL1.address, deployer.address, stakingIncentive, "0x", @@ -676,7 +676,7 @@ describe("StakingBridging", async () => { // Send withheld token info from L2 to L1 when the gas is going to be adjusted from being too high bridgePayload = ethers.utils.defaultAbiCoder.encode(["uint256"], [moreThanMaxUint96]); - await gnosisTargetDispenserL2.syncWithheldTokens(bridgePayload); + await gnosisTargetDispenserL2.syncWithheldAmount(bridgePayload); }); it("Verify senders on L1 and L2", async function () { @@ -704,7 +704,7 @@ describe("StakingBridging", async () => { // Try to receive a message with the wrong sender await expect( - gnosisTargetDispenserL2.syncWithheldTokens(HashZero) + gnosisTargetDispenserL2.syncWithheldAmount(HashZero) ).to.be.revertedWithCustomError(gnosisDepositProcessorL1, "WrongMessageSender"); // Deploy another bridge relayer @@ -781,7 +781,7 @@ describe("StakingBridging", async () => { // Send withheld amount from L2 to L1 with the zero gas limit set bridgePayload = ethers.utils.defaultAbiCoder.encode(["uint256"], [0]); - await optimismTargetDispenserL2.syncWithheldTokens(bridgePayload); + await optimismTargetDispenserL2.syncWithheldAmount(bridgePayload); // Send a message on L2 with funds for a wrong address await dispenser.mintAndSend(optimismDepositProcessorL1.address, deployer.address, stakingIncentive, bridgePayload, @@ -789,14 +789,14 @@ describe("StakingBridging", async () => { // Send withheld amount from L2 to L1 with the more than recommended gas limit bridgePayload = ethers.utils.defaultAbiCoder.encode(["uint256"], [moreThanMaxUint96]); - await optimismTargetDispenserL2.syncWithheldTokens(bridgePayload); + await optimismTargetDispenserL2.syncWithheldAmount(bridgePayload); // Send a message on L2 with funds for a wrong address await dispenser.mintAndSend(optimismDepositProcessorL1.address, deployer.address, stakingIncentive, bridgePayload, stakingIncentive); // Send withheld amount from L2 to L1 without any bridge payload - await optimismTargetDispenserL2.syncWithheldTokens("0x"); + await optimismTargetDispenserL2.syncWithheldAmount("0x"); }); }); @@ -849,7 +849,7 @@ describe("StakingBridging", async () => { expect(Number(withheldAmount)).to.equal(stakingIncentive); // Send withheld amount from L2 to L1 - await polygonTargetDispenserL2.syncWithheldTokens("0x"); + await polygonTargetDispenserL2.syncWithheldAmount("0x"); }); }); @@ -951,7 +951,7 @@ describe("StakingBridging", async () => { // Try to send withheld amount from L2 to L1 with insufficient normalized withheld amount bridgePayload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [deployer.address, 0]); await expect( - wormholeTargetDispenserL2.syncWithheldTokens(bridgePayload, {value: defaultMsgValue}) + wormholeTargetDispenserL2.syncWithheldAmount(bridgePayload, {value: defaultMsgValue}) ).to.be.revertedWithCustomError(wormholeTargetDispenserL2, "ZeroValue"); // Send a message on L2 with funds for a wrong address with a bigger amount @@ -961,12 +961,12 @@ describe("StakingBridging", async () => { // Try to send withheld amount from L2 to L1 with a zero refund address bridgePayload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [AddressZero, 0]); await expect( - wormholeTargetDispenserL2.syncWithheldTokens(bridgePayload, {value: defaultMsgValue}) + wormholeTargetDispenserL2.syncWithheldAmount(bridgePayload, {value: defaultMsgValue}) ).to.be.revertedWithCustomError(wormholeTargetDispenserL2, "ZeroAddress"); // Send withheld amount from L2 to L1 bridgePayload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [deployer.address, 0]); - await wormholeTargetDispenserL2.syncWithheldTokens(bridgePayload, {value: defaultMsgValue}); + await wormholeTargetDispenserL2.syncWithheldAmount(bridgePayload, {value: defaultMsgValue}); }); it("Checks during a message sending on L1 and L2", async function () { @@ -995,13 +995,13 @@ describe("StakingBridging", async () => { // Try to send withheld tokens with an incorrect payload await expect( - wormholeTargetDispenserL2.syncWithheldTokens("0x") + wormholeTargetDispenserL2.syncWithheldAmount("0x") ).to.be.revertedWithCustomError(wormholeTargetDispenserL2, "IncorrectDataLength"); // Try to send withheld tokens without any msg.value covering the cost bridgePayload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [deployer.address, 0]); await expect( - wormholeTargetDispenserL2.syncWithheldTokens(bridgePayload) + wormholeTargetDispenserL2.syncWithheldAmount(bridgePayload) ).to.be.revertedWithCustomError(wormholeTargetDispenserL2, "LowerThan"); }); @@ -1062,13 +1062,13 @@ describe("StakingBridging", async () => { await bridgeRelayer.setMode(3); bridgePayload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [deployer.address, 0]); await expect( - wormholeTargetDispenserL2.syncWithheldTokens(bridgePayload, {value: defaultMsgValue}) + wormholeTargetDispenserL2.syncWithheldAmount(bridgePayload, {value: defaultMsgValue}) ).to.be.revertedWithCustomError(wormholeTargetDispenserL2, "WrongChainId"); // Try to send withheld amount from L2 to L1 with already used hash await bridgeRelayer.setMode(6); // Sync withheld once with the correct nonce - await wormholeTargetDispenserL2.syncWithheldTokens(bridgePayload, {value: defaultMsgValue}); + await wormholeTargetDispenserL2.syncWithheldAmount(bridgePayload, {value: defaultMsgValue}); bridgePayload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [deployer.address, defaultGasLimit]); // Need to create a withheld condition again by sending another staking to a wrong address @@ -1077,7 +1077,7 @@ describe("StakingBridging", async () => { // Now the delivery hash will fail bridgePayload = ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [deployer.address, 0]); await expect( - wormholeTargetDispenserL2.syncWithheldTokens(bridgePayload, {value: defaultMsgValue}) + wormholeTargetDispenserL2.syncWithheldAmount(bridgePayload, {value: defaultMsgValue}) ).to.be.revertedWithCustomError(wormholeTargetDispenserL2, "AlreadyDelivered"); }); });