Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion protocol-contracts/staking/contracts/OperatorStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,12 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
);
}

/**
* @dev Returns a decimal offset of 2 to mitigate the ERC4626 inflation attack.
* This creates 100 virtual shares per asset unit, making the attack economically unfeasible.
*/
function _decimalsOffset() internal view virtual returns (uint8) {
return 0;
return 2;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arr00 we would like your opinion on this change

}

function _getOperatorStakingStorage() private pure returns (OperatorStakingStorage storage $) {
Expand Down
30 changes: 18 additions & 12 deletions protocol-contracts/staking/test/OperatorRewarder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import hre from 'hardhat';
const timeIncreaseNoMine = (duration: number) =>
time.latest().then(clock => time.setNextBlockTimestamp(clock + duration));

// DECIMAL_OFFSET is used in OperatorStaking to mitigate inflation attacks.
// This creates 10^DECIMAL_OFFSET virtual shares per asset unit.
const DECIMAL_OFFSET = 2n;
const VIRTUAL_SHARES = 10n ** DECIMAL_OFFSET;

describe('OperatorRewarder', function () {
beforeEach(async function () {
const [delegator1, delegator2, claimer, admin, beneficiary, anyone, ...accounts] = await ethers.getSigners();
Expand Down Expand Up @@ -156,7 +161,8 @@ describe('OperatorRewarder', function () {
await timeIncreaseNoMine(10);
await this.protocolStaking.connect(this.admin).setRewardRate(0);
await this.mock.connect(this.delegator1).claimRewards(this.delegator1); // claims past rewards before not being able to
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, ethers.parseEther('1'));
const sharesToTransfer = ethers.parseEther('1') * VIRTUAL_SHARES;
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, sharesToTransfer);
// delegator1 will be able deposit and claim reward again
await expect(this.mock.earned(this.delegator1)).to.eventually.eq(0);
// delegator2 cannot claim any reward
Expand Down Expand Up @@ -214,20 +220,17 @@ describe('OperatorRewarder', function () {
await this.mock.connect(this.delegator1).claimRewards(this.delegator1);
await this.mock.connect(this.delegator2).claimRewards(this.delegator2);

const sharesToRedeem = ethers.parseEther('1') * VIRTUAL_SHARES;
await this.operatorStaking
.connect(this.delegator1)
.requestRedeem(ethers.parseEther('1'), this.delegator1, this.delegator1);
.requestRedeem(sharesToRedeem, this.delegator1, this.delegator1);
await this.operatorStaking
.connect(this.delegator2)
.requestRedeem(ethers.parseEther('1'), this.delegator2, this.delegator2);
.requestRedeem(sharesToRedeem, this.delegator2, this.delegator2);
await timeIncreaseNoMine(60);

await this.operatorStaking
.connect(this.delegator1)
.redeem(ethers.parseEther('1'), this.delegator1, this.delegator1);
await this.operatorStaking
.connect(this.delegator2)
.redeem(ethers.parseEther('1'), this.delegator2, this.delegator2);
await this.operatorStaking.connect(this.delegator1).redeem(sharesToRedeem, this.delegator1, this.delegator1);
await this.operatorStaking.connect(this.delegator2).redeem(sharesToRedeem, this.delegator2, this.delegator2);

await this.operatorStaking.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
await expect(this.mock.earned(this.delegator1)).to.eventually.eq(0);
Expand All @@ -252,9 +255,10 @@ describe('OperatorRewarder', function () {

await timeIncreaseNoMine(10);

const sharesToRedeem = ethers.parseEther('2') * VIRTUAL_SHARES;
await this.operatorStaking
.connect(this.delegator1)
.requestRedeem(ethers.parseEther('2'), this.delegator1, this.delegator1);
.requestRedeem(sharesToRedeem, this.delegator1, this.delegator1);

await time.increase(10);

Expand Down Expand Up @@ -576,7 +580,8 @@ describe('OperatorRewarder', function () {
await this.operatorStaking.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
await timeIncreaseNoMine(10);

await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, ethers.parseEther('1'));
const sharesToTransfer = ethers.parseEther('1') * VIRTUAL_SHARES;
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, sharesToTransfer);
await time.increase(10);

await expect(this.mock.earned(this.delegator1)).to.eventually.eq(ethers.parseEther('5'));
Expand All @@ -587,7 +592,8 @@ describe('OperatorRewarder', function () {
await this.operatorStaking.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
await timeIncreaseNoMine(10);

await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, ethers.parseEther('0.5'));
const sharesToTransfer = ethers.parseEther('0.5') * VIRTUAL_SHARES;
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, sharesToTransfer);
await time.increase(10);

await expect(this.mock.earned(this.delegator1)).to.eventually.eq(ethers.parseEther('7.5'));
Expand Down
Loading