Skip to content

Commit 8cf2746

Browse files
authored
Merge pull request #1488 from lidofinance/feat/fix-staking-limit
2 parents 3d55079 + 97cbdb0 commit 8cf2746

File tree

6 files changed

+281
-30
lines changed

6 files changed

+281
-30
lines changed

contracts/0.4.24/Lido.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,10 +1141,9 @@ contract Lido is Versioned, StETHPermit, AragonApp {
11411141
StakeLimitState.Data memory stakeLimitData = STAKING_STATE_POSITION.getStorageStakeLimitStruct();
11421142
if (stakeLimitData.isStakingLimitSet()) {
11431143
uint256 newStakeLimit = stakeLimitData.calculateCurrentStakeLimit() + _amount;
1144-
uint256 maxStakeLimit = stakeLimitData.maxStakeLimit;
11451144

11461145
STAKING_STATE_POSITION.setStorageStakeLimitStruct(
1147-
stakeLimitData.updatePrevStakeLimit(newStakeLimit > maxStakeLimit ? maxStakeLimit : newStakeLimit)
1146+
stakeLimitData.updatePrevStakeLimit(newStakeLimit)
11481147
);
11491148
}
11501149
}

contracts/0.4.24/lib/StakeLimitUtils.sol

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ library StakeLimitUnstructuredStorage {
9393
library StakeLimitUtils {
9494
/**
9595
* @notice Calculate stake limit for the current block.
96-
* @dev using `_constGasMin` to make gas consumption independent of the current block number
96+
* @dev using `_constGasMin`, `_constGasMax`, `_saturatingSub`, `_constGasLt` to make gas consumption independent
97+
* of the current block number
9798
*/
9899
function calculateCurrentStakeLimit(StakeLimitState.Data memory _data) internal view returns(uint256 limit) {
99100
uint256 stakeLimitIncPerBlock;
@@ -102,12 +103,11 @@ library StakeLimitUtils {
102103
}
103104

104105
uint256 blocksPassed = block.number - _data.prevStakeBlockNumber;
105-
uint256 projectedLimit = _data.prevStakeLimit + blocksPassed * stakeLimitIncPerBlock;
106+
uint256 change = blocksPassed * stakeLimitIncPerBlock;
106107

107-
limit = _constGasMin(
108-
projectedLimit,
109-
_data.maxStakeLimit
110-
);
108+
limit = _data.prevStakeLimit < _data.maxStakeLimit ?
109+
_constGasMin(_data.prevStakeLimit + change, _data.maxStakeLimit) :
110+
_constGasMax(_saturatingSub(_data.prevStakeLimit, change), _data.maxStakeLimit);
111111
}
112112

113113
/**
@@ -215,17 +215,47 @@ library StakeLimitUtils {
215215
return _data;
216216
}
217217

218+
/**
219+
* @notice branchless less-than comparison
220+
* @param a first value
221+
* @param b second value
222+
* @return result 1 if a < b, 0 otherwise
223+
*/
224+
function _constGasLt(uint256 a, uint256 b) internal pure returns (uint256 result) {
225+
assembly {
226+
result := lt(a, b)
227+
}
228+
}
229+
218230
/**
219231
* @notice find a minimum of two numbers with a constant gas consumption
220232
* @dev doesn't use branching logic inside
221233
* @param _lhs left hand side value
222234
* @param _rhs right hand side value
223235
*/
224236
function _constGasMin(uint256 _lhs, uint256 _rhs) internal pure returns (uint256 min) {
225-
uint256 lhsIsLess;
226-
assembly {
227-
lhsIsLess := lt(_lhs, _rhs) // lhsIsLess = (_lhs < _rhs) ? 1 : 0
228-
}
237+
uint256 lhsIsLess = _constGasLt(_lhs, _rhs);
229238
min = (_lhs * lhsIsLess) + (_rhs * (1 - lhsIsLess));
230239
}
240+
241+
/**
242+
* @notice find a maximum of two numbers with a constant gas consumption
243+
* @dev doesn't use branching logic inside
244+
* @param _lhs left hand side value
245+
* @param _rhs right hand side value
246+
*/
247+
function _constGasMax(uint256 _lhs, uint256 _rhs) internal pure returns (uint256 max) {
248+
uint256 lhsIsLess = _constGasLt(_lhs, _rhs);
249+
max = (_lhs * (1 - lhsIsLess)) + (_rhs * lhsIsLess);
250+
}
251+
252+
/**
253+
* @notice unsigned saturating subtraction, bounds to zero instead of overflowing
254+
* @param a first value
255+
* @param b second value
256+
*/
257+
function _saturatingSub(uint256 a, uint256 b) internal pure returns (uint256 result) {
258+
uint256 isUnderflow = _constGasLt(a, b);
259+
result = (a - b) * (1 - isUnderflow);
260+
}
231261
}

lib/time.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export async function getNextBlockTimestamp() {
2929
return nextBlockTimestamp;
3030
}
3131

32+
export async function getCurrentBlockNumber() {
33+
return await ethers.provider.getBlockNumber();
34+
}
35+
3236
export async function getNextBlockNumber() {
3337
const latestBlock = BigInt(await time.latestBlock());
3438
return latestBlock + 1n;

test/0.4.24/lib/stakeLimitUtils.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,17 +211,43 @@ describe("StakeLimitUtils.sol", () => {
211211
expect(await stakeLimitUtils.calculateCurrentStakeLimit()).to.equal(staticStakeLimit);
212212
});
213213

214-
it("the full limit gets restored after growth blocks", async () => {
214+
it("the full limit gets restored after growth blocks (increasing to limit)", async () => {
215215
prevStakeBlockNumber = BigInt(await latestBlock());
216216
const baseStakeLimit = 0n;
217217
await stakeLimitUtils.harness_setState(prevStakeBlockNumber, 0n, maxStakeLimitGrowthBlocks, maxStakeLimit);
218+
219+
const growthPerBlock = maxStakeLimit / maxStakeLimitGrowthBlocks;
220+
221+
// 1 block passed due to the setter call above
222+
expect(await stakeLimitUtils.calculateCurrentStakeLimit()).to.equal(growthPerBlock);
223+
224+
// growth blocks passed (might be not equal to maxStakeLimit yet due to rounding)
225+
await mineUpTo(BigInt(prevStakeBlockNumber) + maxStakeLimitGrowthBlocks);
226+
expect(await stakeLimitUtils.calculateCurrentStakeLimit()).to.equal(
227+
baseStakeLimit + maxStakeLimitGrowthBlocks * growthPerBlock,
228+
);
229+
230+
// move forward one more block to account for rounding and reach max
231+
await mineUpTo(BigInt(prevStakeBlockNumber) + maxStakeLimitGrowthBlocks + 1n);
232+
// growth blocks mined, the limit should be full
233+
expect(await stakeLimitUtils.calculateCurrentStakeLimit()).to.equal(maxStakeLimit);
234+
});
235+
236+
it("the full limit gets restored after growth blocks (decreasing to limit)", async () => {
237+
prevStakeBlockNumber = BigInt(await latestBlock());
238+
const initial = maxStakeLimit * 2n;
239+
240+
await stakeLimitUtils.harness_setState(prevStakeBlockNumber, initial, maxStakeLimitGrowthBlocks, maxStakeLimit);
241+
242+
const growthPerBlock = maxStakeLimit / maxStakeLimitGrowthBlocks;
243+
218244
// 1 block passed due to the setter call above
219-
expect(await stakeLimitUtils.calculateCurrentStakeLimit()).to.equal(maxStakeLimit / maxStakeLimitGrowthBlocks);
245+
expect(await stakeLimitUtils.calculateCurrentStakeLimit()).to.equal(initial - growthPerBlock);
220246

221247
// growth blocks passed (might be not equal to maxStakeLimit yet due to rounding)
222248
await mineUpTo(BigInt(prevStakeBlockNumber) + maxStakeLimitGrowthBlocks);
223249
expect(await stakeLimitUtils.calculateCurrentStakeLimit()).to.equal(
224-
baseStakeLimit + maxStakeLimitGrowthBlocks * (maxStakeLimit / maxStakeLimitGrowthBlocks),
250+
initial - maxStakeLimitGrowthBlocks * growthPerBlock,
225251
);
226252

227253
// move forward one more block to account for rounding and reach max

test/0.4.24/lido/lido.externalShares.test.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
66

77
import { ACL, Lido, LidoLocator } from "typechain-types";
88

9-
import { ether, impersonate, MAX_UINT256 } from "lib";
9+
import { advanceChainTime, ether, impersonate, MAX_UINT256 } from "lib";
1010
import { TOTAL_BASIS_POINTS } from "lib/constants";
1111

1212
import { deployLidoDao } from "test/deploy";
@@ -386,17 +386,20 @@ describe("Lido.sol:externalShares", () => {
386386

387387
it("Increases staking limit when burning", async () => {
388388
await lido.setMaxExternalRatioBP(maxExternalRatioBP);
389-
await lido.setStakingLimit(10n, 1n);
389+
await lido.setStakingLimit(10n, 10n);
390390

391391
await lido.connect(vaultHubSigner).mintExternalShares(vaultHubSigner, 1n);
392392

393-
expect(await lido.getCurrentStakeLimit()).to.equal(9n);
393+
let limit = 9n;
394+
expect(await lido.getCurrentStakeLimit()).to.equal(limit);
394395

395396
await lido.connect(vaultHubSigner).burnExternalShares(1n);
396-
expect(await lido.getCurrentStakeLimit()).to.equal(10n);
397+
limit += 1n; // for mining block with burning
398+
399+
expect(await lido.getCurrentStakeLimit()).to.equal(limit + 1n);
397400
});
398401

399-
it("Restores staking limit to max when burning more", async () => {
402+
it("Bypasses staking limit when burning more than staking limit", async () => {
400403
await lido.setMaxExternalRatioBP(maxExternalRatioBP);
401404
await lido.connect(vaultHubSigner).mintExternalShares(vaultHubSigner, 5n);
402405

@@ -407,10 +410,15 @@ describe("Lido.sol:externalShares", () => {
407410
const amountToMint = await lido.getPooledEthByShares(sharesToMint);
408411
await lido.connect(vaultHubSigner).mintExternalShares(vaultHubSigner, sharesToMint);
409412

410-
expect(await lido.getCurrentStakeLimit()).to.equal(10n - amountToMint);
413+
let limit = 10n - amountToMint;
414+
expect(await lido.getCurrentStakeLimit()).to.equal(limit);
411415

412-
await lido.connect(vaultHubSigner).burnExternalShares(10n);
413-
expect(await lido.getCurrentStakeLimit()).to.equal(10n);
416+
const sharesToBurn = 10n;
417+
const amountToBurn = await lido.getPooledEthByShares(sharesToBurn);
418+
await lido.connect(vaultHubSigner).burnExternalShares(sharesToBurn);
419+
limit += 1n; // for mining block with burning
420+
421+
expect(await lido.getCurrentStakeLimit()).to.equal(limit + amountToBurn);
414422
});
415423
});
416424

@@ -487,18 +495,16 @@ describe("Lido.sol:externalShares", () => {
487495

488496
it("Can mint and burn external shares without limit change after multiple loops", async () => {
489497
await lido.setMaxExternalRatioBP(maxExternalRatioBP);
490-
await lido.setStakingLimit(1000n, 1n);
498+
await lido.setStakingLimit(1000n, 100n);
491499

492500
for (let i = 1n; i <= 500n; i++) {
493-
const stakingLimitBefore = await lido.getCurrentStakeLimit();
494-
expect(stakingLimitBefore).to.equal(1000n);
495-
496501
await lido.connect(vaultHubSigner).mintExternalShares(vaultHubSigner, i);
497502
await lido.connect(vaultHubSigner).burnExternalShares(i);
498-
499-
const stakingLimitAfter = await lido.getCurrentStakeLimit();
500-
expect(stakingLimitAfter).to.equal(stakingLimitBefore);
501503
}
504+
505+
// need to mine a block to update the stake limit otherwise it will be 1000n + 100n (after burning)
506+
await advanceChainTime(1n);
507+
expect(await lido.getCurrentStakeLimit()).to.equal(1000n);
502508
});
503509
});
504510

0 commit comments

Comments
 (0)