Skip to content

Commit ac952f6

Browse files
yahgwaigzeoneth
andauthored
feat: bold reduce base stake (#391)
* feat: added base stake reduction with additional check * style: formatting * test: move missing rollup tests into foundry * test: Added tests for setting base stake * refactor: simplify confirmed stakers check * doc: clarified comment about permissioned stakrs * style: formatting * feat: separate increase/decrease base stake and added additional checks and tests for them * feat: remove getAllStakers public function and instead make stakerlist internal * chore: remove unused comment * fix: yarn format * chore: remove redundent import * chore: update slither db * chore: update 4bytes * refactor: use getStakerAddress * chore: avoid shadowing * chore: keep getStaker external --------- Co-authored-by: gzeon <im@gzeon.dev> Co-authored-by: gzeon <hng@offchainlabs.com>
1 parent 426e3c8 commit ac952f6

File tree

10 files changed

+280
-45
lines changed

10 files changed

+280
-45
lines changed

foundry.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ additional_compiler_profiles = [
2525
]
2626
compilation_restrictions = [
2727
{ paths = "src/rollup/RollupUserLogic.sol", optimizer_runs = 20 },
28+
{ paths = "src/rollup/RollupAdminLogic.sol", optimizer_runs = 20 },
2829
{ paths = "src/challengeV2/EdgeChallengeManager.sol", optimizer_runs = 200 },
2930
]
3031
skip = ['test/*']

hardhat.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ const solidity: SolidityConfig = {
3838
optimizer: { ...commonSetting.optimizer, runs: 20 },
3939
},
4040
},
41+
'src/rollup/RollupAdminLogic.sol': {
42+
version: '0.8.17',
43+
settings: {
44+
...commonSetting,
45+
optimizer: { ...commonSetting.optimizer, runs: 20 },
46+
},
47+
},
4148
'src/challengeV2/EdgeChallengeManager.sol': {
4249
version: '0.8.17',
4350
settings: {

slither.db.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/rollup/BOLDUpgradeAction.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import "@openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol";
88
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
99
import "./RollupProxy.sol";
1010
import "./RollupLib.sol";
11-
import "./RollupAdminLogic.sol";
1211

1312
struct Node {
1413
// Hash of the state of the chain as of this node

src/rollup/IRollupAdmin.sol

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,22 @@ interface IRollupAdmin {
150150
) external;
151151

152152
/**
153-
* @notice Set base stake required for an assertion
154-
* @param newBaseStake maximum avmgas to be used per block
153+
* @notice Decrease the base stake required for creating an assertion
154+
* @dev Can only be called on permissioned chains with whitelist enabled
155+
* Can only be called when there is exactly one stake on a pending assertion - this ensures a linear chain of assertions
156+
* Cannot be called immediately after genesis since there are no pending assertions
157+
* After decreasing the base stake the current staker will still have full stake locked up. They can release it by creating a new staker with the
158+
* new smaller amount, and using it to create a child of the latest pending assertion. This will make the old staker inactive and withdrawable.
159+
* @param newBaseStake New base stake to be set. Must be less than current base stake, otherwise use increaseBaseStake
160+
* @param latestNextInboxPosition The nextInboxPosition of the only pending latestStakedAssertion
155161
*/
156-
function setBaseStake(
162+
function decreaseBaseStake(uint256 newBaseStake, uint64 latestNextInboxPosition) external;
163+
164+
/**
165+
* @notice Increase the base stake required for creating an assertion
166+
* @param newBaseStake New base stake to be set. Must be greater than current base stake, otherwise use reduceBaseStake
167+
*/
168+
function increaseBaseStake(
157169
uint256 newBaseStake
158170
) external;
159171

src/rollup/RollupAdminLogic.sol

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import "./RollupCore.sol";
1010
import "../bridge/IOutbox.sol";
1111
import "../bridge/ISequencerInbox.sol";
1212
import "../libraries/DoubleLogicUUPSUpgradeable.sol";
13-
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
1413

1514
contract RollupAdminLogic is RollupCore, IRollupAdmin, DoubleLogicUUPSUpgradeable {
1615
using AssertionStateLib for AssertionState;
@@ -264,20 +263,80 @@ contract RollupAdminLogic is RollupCore, IRollupAdmin, DoubleLogicUUPSUpgradeabl
264263
}
265264

266265
/**
267-
* @notice Set base stake required for an assertion
268-
* @param newBaseStake minimum amount of stake required
266+
* @inheritdoc IRollupAdmin
269267
*/
270-
function setBaseStake(
271-
uint256 newBaseStake
268+
function decreaseBaseStake(
269+
uint256 newBaseStake,
270+
uint64 latestNextInboxPosition
272271
) external override {
273-
// we do not currently allow base stake to be reduced since as doing so might allow a malicious party
272+
require(newBaseStake < baseStake, "BASE_STAKE_NOT_DECREASED");
273+
274+
// if we're decreasing the stake we need to be more careful not to allow a malicious party
274275
// to withdraw some (up to the difference between baseStake and newBaseStake) honest funds from this contract
275276
// The sequence of events is as follows:
276277
// 1. The malicious party creates a sibling assertion, stake size is currently S
277-
// 2. The base stake is then reduced to S'
278+
// 2. The base stake is then decreased to S'
278279
// 3. The malicious party uses a different address to create a child of the malicious assertion, using stake size S'
279280
// 4. This allows the malicious party to withdraw the stake S, since assertions with children set the staker to "inactive"
280-
require(newBaseStake > baseStake, "BASE_STAKE_MUST_BE_INCREASED");
281+
282+
// for this reason the base stake can only be decreased on permissioned chains where the creation of pending assertions can be controlled
283+
// In a permissionless setting, it's possible for a staker to DOS base stake decrease by creating themselves as a staker.
284+
// we can do a basic safety check to ensure that we're in the permissioned setting by checking that there is only one
285+
// staker on a pending assertion. It could be that we're in the permissionless setting and that pending assertion is a malicious one
286+
// but we assume that the rollup admin will ensure that's not the case before calling this function by checking that a staker they trust is
287+
// on a correct pending assertion.
288+
require(validatorWhitelistDisabled == false, "DECREASE_ONLY_FOR_PERMISSIONED_CHAINS");
289+
290+
bytes32 currentConfigHash = RollupLib.configHash({
291+
wasmModuleRoot: wasmModuleRoot,
292+
requiredStake: baseStake,
293+
challengeManager: address(challengeManager),
294+
confirmPeriodBlocks: confirmPeriodBlocks,
295+
nextInboxPosition: uint64(latestNextInboxPosition)
296+
});
297+
298+
uint256 pendingCount = 0;
299+
uint64 nStakers = stakerCount();
300+
for (uint64 i = 0; i < nStakers; i++) {
301+
bytes32 latestStaked = latestStakedAssertion(getStakerAddress(i));
302+
AssertionNode storage latestAssertion = getAssertionStorage(latestStaked);
303+
if (latestAssertion.status == AssertionStatus.Pending) {
304+
// check that in overwriting the config hash, we'll only be updating the required stake field
305+
require(latestAssertion.configHash == currentConfigHash, "EXPIRED_CONFIG_HASH");
306+
// In order to support transitioning from permissioned to permissionless, we need to ensure that after the transition
307+
// a malicious party cannot created decreased stake assertions as children of historical (even confirmed) assertions.
308+
// To avoid this an additional check has been added in RollupUserLogic.createNewAssertion to ensure that the base stake
309+
// is always >= the required stake of the prev assertion. Having added this check we also need to override the config hash of
310+
// the latest staked assertion so that children can be created from it in the normal way. We do that below
311+
latestAssertion.configHash = RollupLib.configHash({
312+
wasmModuleRoot: wasmModuleRoot,
313+
requiredStake: newBaseStake,
314+
challengeManager: address(challengeManager),
315+
confirmPeriodBlocks: confirmPeriodBlocks,
316+
nextInboxPosition: uint64(latestNextInboxPosition)
317+
});
318+
319+
pendingCount++;
320+
}
321+
322+
require(pendingCount <= 1, "TOO_MANY_PENDING_STAKERS");
323+
}
324+
325+
// we need to have a non zero pending count to ensure that exactly one config hash was updated
326+
// if no config hashes are updated then no new assertions will be created, since they will all trigger STAKE_TOO_LOW
327+
require(pendingCount == 1, "PENDING_ASSERTION_NOT_UPDATED");
328+
329+
baseStake = newBaseStake;
330+
emit BaseStakeSet(newBaseStake);
331+
}
332+
333+
/**
334+
* @inheritdoc IRollupAdmin
335+
*/
336+
function increaseBaseStake(
337+
uint256 newBaseStake
338+
) external override {
339+
require(newBaseStake > baseStake, "BASE_STAKE_NOT_INCREASED");
281340
baseStake = newBaseStake;
282341
emit BaseStakeSet(newBaseStake);
283342
// previously: emit OwnerFunctionCalled(12);

src/rollup/RollupCore.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ abstract contract RollupCore is IRollupCore, PausableUpgradeable {
171171
*/
172172
function getStakerAddress(
173173
uint64 stakerNum
174-
) external view override returns (address) {
174+
) public view override returns (address) {
175175
return _stakerList[stakerNum];
176176
}
177177

src/rollup/RollupUserLogic.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ contract RollupUserLogic is RollupCore, UUPSNotUpgradeable, IRollupUser {
184184
"INSUFFICIENT_STAKE"
185185
);
186186

187+
// the new assertion will be created with requiredStake equal to the baseStake
188+
// however, for the reasons explained in RollupAdminLogic.reduceBaseStake we cannot allow an assertion to be created
189+
// base stake than it's prev. In order to reduce the base stake the config hash of the latest assertion
190+
// must be overridden, this occurs in the RollupAdminLogic.reduceBaseStake function
191+
require(baseStake >= assertion.beforeStateData.configData.requiredStake, "STAKE_TOO_LOW");
192+
187193
bytes32 prevAssertion = RollupLib.assertionHash(
188194
assertion.beforeStateData.prevPrevAssertionHash,
189195
assertion.beforeState,

0 commit comments

Comments
 (0)