Skip to content

Commit f9e7d2c

Browse files
authored
fix(protocol-contracts): mitigate ERC4626 inflation attack with decimal offset (L-06) (#1684)
1 parent a3f4d02 commit f9e7d2c

File tree

3 files changed

+100
-95
lines changed

3 files changed

+100
-95
lines changed

protocol-contracts/staking/contracts/OperatorStaking.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,12 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
474474
);
475475
}
476476

477+
/**
478+
* @dev Returns a decimal offset of 2 to mitigate the ERC4626 inflation attack.
479+
* This creates 100 virtual shares per asset unit, making the attack economically unfeasible.
480+
*/
477481
function _decimalsOffset() internal view virtual returns (uint8) {
478-
return 0;
482+
return 2;
479483
}
480484

481485
function _getOperatorStakingStorage() internal pure returns (OperatorStakingStorage storage $) {

protocol-contracts/staking/test/OperatorRewarder.test.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import hre from 'hardhat';
77
const timeIncreaseNoMine = (duration: number) =>
88
time.latest().then(clock => time.setNextBlockTimestamp(clock + duration));
99

10+
// DECIMAL_OFFSET is used in OperatorStaking to mitigate inflation attacks.
11+
// This creates 10^DECIMAL_OFFSET virtual shares per asset unit.
12+
const DECIMAL_OFFSET = 2n;
13+
const SHARES_PER_ASSET_UNIT = 10n ** DECIMAL_OFFSET;
14+
1015
describe('OperatorRewarder', function () {
1116
beforeEach(async function () {
1217
const [delegator1, delegator2, claimer, admin, beneficiary, anyone, ...accounts] = await ethers.getSigners();
@@ -162,7 +167,8 @@ describe('OperatorRewarder', function () {
162167
await timeIncreaseNoMine(10);
163168
await this.protocolStaking.connect(this.admin).setRewardRate(0);
164169
await this.mock.connect(this.delegator1).claimRewards(this.delegator1); // claims past rewards before not being able to
165-
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, ethers.parseEther('1'));
170+
const sharesToTransfer = ethers.parseEther('1') * SHARES_PER_ASSET_UNIT;
171+
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, sharesToTransfer);
166172
// delegator1 will be able deposit and claim reward again
167173
await expect(this.mock.earned(this.delegator1)).to.eventually.eq(0);
168174
// delegator2 cannot claim any reward
@@ -223,20 +229,17 @@ describe('OperatorRewarder', function () {
223229
await this.mock.connect(this.delegator1).claimRewards(this.delegator1);
224230
await this.mock.connect(this.delegator2).claimRewards(this.delegator2);
225231

232+
const sharesToRedeem = ethers.parseEther('1') * SHARES_PER_ASSET_UNIT;
226233
await this.operatorStaking
227234
.connect(this.delegator1)
228-
.requestRedeem(ethers.parseEther('1'), this.delegator1, this.delegator1);
235+
.requestRedeem(sharesToRedeem, this.delegator1, this.delegator1);
229236
await this.operatorStaking
230237
.connect(this.delegator2)
231-
.requestRedeem(ethers.parseEther('1'), this.delegator2, this.delegator2);
238+
.requestRedeem(sharesToRedeem, this.delegator2, this.delegator2);
232239
await timeIncreaseNoMine(60);
233240

234-
await this.operatorStaking
235-
.connect(this.delegator1)
236-
.redeem(ethers.parseEther('1'), this.delegator1, this.delegator1);
237-
await this.operatorStaking
238-
.connect(this.delegator2)
239-
.redeem(ethers.parseEther('1'), this.delegator2, this.delegator2);
241+
await this.operatorStaking.connect(this.delegator1).redeem(sharesToRedeem, this.delegator1, this.delegator1);
242+
await this.operatorStaking.connect(this.delegator2).redeem(sharesToRedeem, this.delegator2, this.delegator2);
240243

241244
await this.operatorStaking.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
242245
await expect(this.mock.earned(this.delegator1)).to.eventually.eq(0);
@@ -261,9 +264,10 @@ describe('OperatorRewarder', function () {
261264

262265
await timeIncreaseNoMine(10);
263266

267+
const sharesToRedeem = ethers.parseEther('2') * SHARES_PER_ASSET_UNIT;
264268
await this.operatorStaking
265269
.connect(this.delegator1)
266-
.requestRedeem(ethers.parseEther('2'), this.delegator1, this.delegator1);
270+
.requestRedeem(sharesToRedeem, this.delegator1, this.delegator1);
267271

268272
await time.increase(10);
269273

@@ -591,7 +595,8 @@ describe('OperatorRewarder', function () {
591595
await this.operatorStaking.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
592596
await timeIncreaseNoMine(10);
593597

594-
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, ethers.parseEther('1'));
598+
const sharesToTransfer = ethers.parseEther('1') * SHARES_PER_ASSET_UNIT;
599+
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, sharesToTransfer);
595600
await time.increase(10);
596601

597602
await expect(this.mock.earned(this.delegator1)).to.eventually.eq(ethers.parseEther('5'));
@@ -602,7 +607,8 @@ describe('OperatorRewarder', function () {
602607
await this.operatorStaking.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
603608
await timeIncreaseNoMine(10);
604609

605-
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, ethers.parseEther('0.5'));
610+
const sharesToTransfer = ethers.parseEther('0.5') * SHARES_PER_ASSET_UNIT;
611+
await this.operatorStaking.connect(this.delegator1).transfer(this.delegator2, sharesToTransfer);
606612
await time.increase(10);
607613

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

0 commit comments

Comments
 (0)