Skip to content

Commit

Permalink
Merge pull request kroma-network#104 from kroma-network/feat/unbond-m…
Browse files Browse the repository at this point in the history
…ultiple-bonds

feat(contracts): unbond multiple bonds
  • Loading branch information
Pangssu authored Jul 10, 2023
2 parents fbc618e + 10d6264 commit 5b517e1
Show file tree
Hide file tree
Showing 16 changed files with 352 additions and 135 deletions.
39 changes: 35 additions & 4 deletions bindings/bindings/validatorpool.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bindings/bindings/validatorpool_more.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions e2e/e2eutils/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams {

ValidatorPoolTrustedValidator: addresses.TrustedValidator,
ValidatorPoolMinBondAmount: uint64ToBig(1),
ValidatorPoolMaxUnbond: 10,
ValidatorPoolNonPenaltyPeriod: 3,
ValidatorPoolPenaltyPeriod: 3,

Expand Down
1 change: 1 addition & 0 deletions e2e/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {

ValidatorPoolTrustedValidator: addresses.TrustedValidator,
ValidatorPoolMinBondAmount: uint642big(1),
ValidatorPoolMaxUnbond: 10,
ValidatorPoolNonPenaltyPeriod: 2,
ValidatorPoolPenaltyPeriod: 2,

Expand Down
200 changes: 101 additions & 99 deletions packages/contracts/.gas-snapshot

Large diffs are not rendered by default.

64 changes: 45 additions & 19 deletions packages/contracts/contracts/L1/ValidatorPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ contract ValidatorPool is ReentrancyGuardUpgradeable, Semver {
*/
uint256 public immutable MIN_BOND_AMOUNT;

/**
* @notice The max number of unbonds when trying unbond.
*/
uint256 public immutable MAX_UNBOND;

/**
* @notice The period in a round that the penalty does not apply, after which it does (in seconds).
*/
Expand All @@ -57,7 +62,7 @@ contract ValidatorPool is ReentrancyGuardUpgradeable, Semver {
*/
uint256 public immutable PENALTY_PERIOD;

/*
/**
* @notice The duration of a submission round for one output (in seconds).
* Note that there are two submission rounds for an output: PRIORITY ROUND and PUBLIC ROUND.
*/
Expand Down Expand Up @@ -133,6 +138,7 @@ contract ValidatorPool is ReentrancyGuardUpgradeable, Semver {
* @param _portal Address of the KromaPortal.
* @param _trustedValidator Address of the trusted validator.
* @param _minBondAmount The minimum bond amount.
* @param _maxUnbond The max number of unbonds when trying unbond.
* @param _nonPenaltyPeriod The period during a submission round that is not penalized.
* @param _penaltyPeriod The period during a submission round when penalties are applied.
*/
Expand All @@ -141,13 +147,15 @@ contract ValidatorPool is ReentrancyGuardUpgradeable, Semver {
KromaPortal _portal,
address _trustedValidator,
uint256 _minBondAmount,
uint256 _maxUnbond,
uint256 _nonPenaltyPeriod,
uint256 _penaltyPeriod
) Semver(0, 1, 0) {
L2_ORACLE = _l2OutputOracle;
PORTAL = _portal;
TRUSTED_VALIDATOR = _trustedValidator;
MIN_BOND_AMOUNT = _minBondAmount;
MAX_UNBOND = _maxUnbond;
NON_PENALTY_PERIOD = _nonPenaltyPeriod;
PENALTY_PERIOD = _penaltyPeriod;

Expand Down Expand Up @@ -178,7 +186,7 @@ contract ValidatorPool is ReentrancyGuardUpgradeable, Semver {
function withdraw(uint256 _amount) external nonReentrant {
_decreaseBalance(msg.sender, _amount);

bool success = SafeCall.call(msg.sender, gasleft(), _amount, hex"");
bool success = SafeCall.call(msg.sender, gasleft(), _amount, "");
require(success, "ValidatorPool: ETH transfer failed");
}

Expand Down Expand Up @@ -243,32 +251,50 @@ contract ValidatorPool is ReentrancyGuardUpgradeable, Semver {
}

/**
* @notice Attempts to unbond corresponding to nextUnbondOutputIndex and returns whether the unbond was successful.
* When unbound, it updates the next priority validator and sends a reward message to L2.
* @notice Attempts to unbond starting from nextUnbondOutputIndex and returns whether at least
* one unbond is executed. Tries unbond at most MAX_UNBOND number of bonds and sends
* a reward message to L2 for each unbond.
* Note that it updates the next priority validator using last unbond, and not updates
* when no unbond.
*
* @return Whether the bond has been successfully unbonded.
* @return Whether at least one unbond is executed.
*/
function _tryUnbond() private returns (bool) {
uint256 outputIndex = nextUnbondOutputIndex;

Types.Bond storage bond = bonds[outputIndex];
uint128 bondAmount = bond.amount;
if (block.timestamp >= bond.expiresAt && bondAmount > 0) {
delete bonds[outputIndex];

Types.CheckpointOutput memory output = L2_ORACLE.getL2Output(outputIndex);
_increaseBalance(output.submitter, bondAmount);
emit Unbonded(outputIndex, output.submitter, bondAmount);

unchecked {
++nextUnbondOutputIndex;
uint128 bondAmount;
Types.Bond storage bond;
Types.CheckpointOutput memory output;

uint256 unbondedNum = 0;
for (; unbondedNum < MAX_UNBOND; ) {
bond = bonds[outputIndex];
bondAmount = bond.amount;

if (block.timestamp >= bond.expiresAt && bondAmount > 0) {
delete bonds[outputIndex];
output = L2_ORACLE.getL2Output(outputIndex);
_increaseBalance(output.submitter, bondAmount);
emit Unbonded(outputIndex, output.submitter, bondAmount);

// Send reward message to L2 ValidatorRewardVault.
_sendRewardMessageToL2Vault(output);

unchecked {
++unbondedNum;
++outputIndex;
}
} else {
break;
}
}

if (unbondedNum > 0) {
// Select the next priority validator.
_updatePriorityValidator(output.outputRoot);
// Send reward message to L2 ValidatorRewardVault.
_sendRewardMessageToL2Vault(output);

unchecked {
nextUnbondOutputIndex = outputIndex;
}
return true;
}

Expand Down
6 changes: 4 additions & 2 deletions packages/contracts/contracts/test/CommonTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,21 @@ contract L2OutputOracle_Initializer is CommonTest {
// ValidatorPool constructor arguments
address internal trusted = 0x000000000000000000000000000000000000aaaa;
uint256 internal minBond = 0.1 ether;
uint256 internal maxUnbond = 2;
uint256 internal nonPenaltyPeriod = 10 minutes;
uint256 internal penaltyPeriod = 20 minutes;
uint256 internal roundDuration = nonPenaltyPeriod + penaltyPeriod;

// Constructor arguments
address internal asserter = 0x000000000000000000000000000000000000aAaB;
address internal challenger = 0x000000000000000000000000000000000000AAaC;
uint256 internal submissionInterval = 1800;
uint256 internal l2BlockTime = 2;
uint256 internal startingBlockNumber = 200;
uint256 internal startingTimestamp = 1000;
address internal guardian = 0x000000000000000000000000000000000000AaaD;

// Test data
address internal asserter = 0x000000000000000000000000000000000000aAaB;
address internal challenger = 0x000000000000000000000000000000000000AAaC;
uint256 initL1Time;

event OutputSubmitted(
Expand Down Expand Up @@ -176,6 +177,7 @@ contract L2OutputOracle_Initializer is CommonTest {
_portal: mockPortal,
_trustedValidator: trusted,
_minBondAmount: minBond,
_maxUnbond: maxUnbond,
_nonPenaltyPeriod: nonPenaltyPeriod,
_penaltyPeriod: penaltyPeriod
});
Expand Down
130 changes: 125 additions & 5 deletions packages/contracts/contracts/test/ValidatorPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ contract ValidatorPoolTest is L2OutputOracle_Initializer {
assertEq(address(pool.L2_ORACLE()), address(oracle));
assertEq(pool.TRUSTED_VALIDATOR(), trusted);
assertEq(pool.MIN_BOND_AMOUNT(), minBond);
assertEq(pool.MAX_UNBOND(), maxUnbond);
assertEq(pool.NON_PENALTY_PERIOD(), nonPenaltyPeriod);
assertEq(pool.PENALTY_PERIOD(), penaltyPeriod);
assertEq(pool.ROUND_DURATION(), nonPenaltyPeriod + penaltyPeriod);
Expand Down Expand Up @@ -193,7 +194,7 @@ contract ValidatorPoolTest is L2OutputOracle_Initializer {

uint128 expiresAt = uint128(block.timestamp + finalizationPeriodSeconds);
vm.prank(address(oracle));
vm.expectEmit(true, true, false, false);
vm.expectEmit(true, true, false, true, address(pool));
emit Bonded(validator, nextOutputIndex, uint128(minBond), expiresAt);
pool.createBond(nextOutputIndex, uint128(minBond), expiresAt);
assertEq(pool.balanceOf(validator), 0);
Expand Down Expand Up @@ -226,7 +227,7 @@ contract ValidatorPoolTest is L2OutputOracle_Initializer {

uint128 expiresAt = uint128(block.timestamp + finalizationPeriodSeconds);
vm.prank(address(oracle));
vm.expectEmit(true, true, false, false);
vm.expectEmit(true, true, false, true, address(pool));
emit Unbonded(0, firstOutput.submitter, uint128(firstBond.amount));
pool.createBond(nextOutputIndex, uint128(minBond), expiresAt);
assertEq(pool.balanceOf(firstOutput.submitter), minBond);
Expand Down Expand Up @@ -303,7 +304,7 @@ contract ValidatorPoolTest is L2OutputOracle_Initializer {
// in this test, the penalty is always 0 because the output was submitted without delay.
uint256 penalty = 0;

vm.expectEmit(true, true, false, false);
vm.expectEmit(true, true, false, true, address(pool));
emit Unbonded(latestOutputIndex, output.submitter, bond.amount);
vm.expectCall(
address(pool.PORTAL()),
Expand Down Expand Up @@ -363,7 +364,7 @@ contract ValidatorPoolTest is L2OutputOracle_Initializer {
assertEq(penalty, delay - nonPenaltyPeriod);
}

vm.expectEmit(true, true, false, false);
vm.expectEmit(true, true, false, true, address(pool));
emit Unbonded(outputIndex, validator, bondAmount);
vm.expectCall(
address(pool.PORTAL()),
Expand All @@ -385,6 +386,125 @@ contract ValidatorPoolTest is L2OutputOracle_Initializer {
assertEq(pool.balanceOf(validator), bondAmount);
}

function test_unbond_multipleBonds_succeeds() public {
uint256 tries = 2;
uint256 deposit = minBond * tries;
vm.prank(trusted);
pool.deposit{ value: deposit }();

// submit 2 outputs, only trusted can submit outputs before at least one unbond.
uint256 blockNumber = 0;
uint128 expiresAt = 0;
for (uint256 i = 0; i < tries; i++) {
blockNumber = oracle.nextBlockNumber();
warpToSubmitTime(blockNumber);
expiresAt = uint128(block.timestamp + finalizationPeriodSeconds);
assertEq(pool.nextValidator(), trusted);
vm.prank(trusted);
mockOracle.addOutput(keccak256(abi.encode(blockNumber)), blockNumber);
vm.prank(address(oracle));
pool.createBond(i, uint128(minBond), expiresAt);
assertEq(pool.balanceOf(trusted), deposit - minBond * (i + 1));
}

uint256 firstOutputIndex = 0;
Types.CheckpointOutput memory firstOutput = oracle.getL2Output(firstOutputIndex);
Types.Bond memory firstBond = pool.getBond(firstOutputIndex);

uint256 secondOutputIndex = 1;
Types.CheckpointOutput memory secondOutput = oracle.getL2Output(secondOutputIndex);
Types.Bond memory secondBond = pool.getBond(secondOutputIndex);

// warp to the time the second output is finalized and the two bonds are expired.
vm.warp(secondBond.expiresAt);

// in this test, the penalty is always 0 because the outputs were submitted without delay.
uint256 penalty = 0;

vm.expectEmit(true, true, false, true, address(pool));
emit Unbonded(firstOutputIndex, firstOutput.submitter, firstBond.amount);
vm.expectCall(
address(pool.PORTAL()),
abi.encodeWithSelector(
KromaPortal.depositTransactionByValidatorPool.selector,
Predeploys.VALIDATOR_REWARD_VAULT,
pool.VAULT_REWARD_GAS_LIMIT(),
abi.encodeWithSelector(
ValidatorRewardVault.reward.selector,
firstOutput.submitter,
firstOutput.l2BlockNumber,
penalty,
penaltyPeriod
)
)
);
vm.expectEmit(true, true, false, true, address(pool));
emit Unbonded(secondOutputIndex, secondOutput.submitter, secondBond.amount);
vm.expectCall(
address(pool.PORTAL()),
abi.encodeWithSelector(
KromaPortal.depositTransactionByValidatorPool.selector,
Predeploys.VALIDATOR_REWARD_VAULT,
pool.VAULT_REWARD_GAS_LIMIT(),
abi.encodeWithSelector(
ValidatorRewardVault.reward.selector,
secondOutput.submitter,
secondOutput.l2BlockNumber,
penalty,
penaltyPeriod
)
)
);
vm.prank(trusted);
pool.unbond();

// check whether bonds are deleted and trusted balance has increased.
for (uint256 i = 0; i < tries; i++) {
vm.expectRevert("ValidatorPool: the bond does not exist");
pool.getBond(i);
}
assertEq(pool.balanceOf(trusted), deposit);
}

function test_unbond_maxUnbond_succeeds() public {
uint256 tries = maxUnbond + 1;
uint256 deposit = minBond * tries;
vm.prank(trusted);
pool.deposit{ value: deposit }();

// submit (maxUnbond + 1) outputs, only trusted can submit outputs before at least one unbond.
uint256 blockNumber = 0;
uint128 expiresAt = 0;
for (uint256 i = 0; i < tries; i++) {
blockNumber = oracle.nextBlockNumber();
warpToSubmitTime(blockNumber);
expiresAt = uint128(block.timestamp + finalizationPeriodSeconds);
assertEq(pool.nextValidator(), trusted);
vm.prank(trusted);
mockOracle.addOutput(keccak256(abi.encode(blockNumber)), blockNumber);
vm.prank(address(oracle));
pool.createBond(i, uint128(minBond), expiresAt);
assertEq(pool.balanceOf(trusted), deposit - minBond * (i + 1));
}

uint256 latestOutputIndex = oracle.latestOutputIndex();
Types.Bond memory bond = pool.getBond(latestOutputIndex);

// warp to the time the latest output is finalized and all bonds are expired.
vm.warp(bond.expiresAt);

vm.prank(trusted);
pool.unbond();

// check whether maxUnbond number of bonds are deleted and the last one is not.
for (uint256 i = 0; i < tries - 1; i++) {
vm.expectRevert("ValidatorPool: the bond does not exist");
pool.getBond(i);
}
bond = pool.getBond(tries - 1);
assertEq(bond.amount, minBond);
}

function test_unbond_notExpired_reverts() external {
test_createBond_succeeds();

Expand All @@ -407,7 +527,7 @@ contract ValidatorPoolTest is L2OutputOracle_Initializer {
pool.deposit{ value: prevBond.amount }();

vm.prank(oracle.COLOSSEUM());
vm.expectEmit(true, true, false, false);
vm.expectEmit(true, true, false, true, address(pool));
emit BondIncreased(challenger, latestOutputIndex, prevBond.amount);
pool.increaseBond(challenger, latestOutputIndex);

Expand Down
7 changes: 6 additions & 1 deletion packages/contracts/deploy-config/devnetL1.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"batchSenderAddress": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"validatorPoolTrustedValidator": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"validatorPoolMinBondAmount": "0x1",
"validatorPoolMaxUnbond": 10,
"validatorPoolNonPenaltyPeriod": 10,
"validatorPoolPenaltyPeriod": 10,
"l2OutputOracleSubmissionInterval": 20,
Expand Down Expand Up @@ -40,5 +41,9 @@
"colosseumSegmentsLengths": "3,11",
"numDeployConfirmations": 1,
"securityCouncilNumConfirmationRequired": 1,
"securityCouncilOwners": ["0x007D7e4391DCFdE47Dd7FD0B8E16091C5e0E1C7f","0x2A06Af6a4F325B1e0095a8AbD32888e0AbdCD04D","0xed92E7C40348A552822847384D722A6adB9AFeFA"]
"securityCouncilOwners": [
"0x007D7e4391DCFdE47Dd7FD0B8E16091C5e0E1C7f",
"0x2A06Af6a4F325B1e0095a8AbD32888e0AbdCD04D",
"0xed92E7C40348A552822847384D722A6adB9AFeFA"
]
}
1 change: 1 addition & 0 deletions packages/contracts/deploy-config/sepolia.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"batchSenderAddress": "0xf15dc770221b99c98d4aaed568f2ab04b9d16e42",
"validatorPoolTrustedValidator": "0xc2da150ecfaa2e275577203bb177dd4de4a2536e",
"validatorPoolMinBondAmount": "0x16345785d8a0000",
"validatorPoolMaxUnbond": 10,
"validatorPoolNonPenaltyPeriod": 20,
"validatorPoolPenaltyPeriod": 40,
"protocolVaultRecipient": "0xb5d7c1921fdbdea614f264a75ca0be416cf63eb5",
Expand Down
6 changes: 6 additions & 0 deletions packages/contracts/deploy/007-ValidatorPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const deployFn: DeployFunction = async (hre) => {
portalProxyAddress,
hre.deployConfig.validatorPoolTrustedValidator,
hre.deployConfig.validatorPoolMinBondAmount,
hre.deployConfig.validatorPoolMaxUnbond,
hre.deployConfig.validatorPoolNonPenaltyPeriod,
hre.deployConfig.validatorPoolPenaltyPeriod,
],
Expand All @@ -43,6 +44,11 @@ const deployFn: DeployFunction = async (hre) => {
'MIN_BOND_AMOUNT',
hre.deployConfig.validatorPoolMinBondAmount
)
await assertContractVariable(
contract,
'MAX_UNBOND',
hre.deployConfig.validatorPoolMaxUnbond
)
await assertContractVariable(
contract,
'NON_PENALTY_PERIOD',
Expand Down
Loading

0 comments on commit 5b517e1

Please sign in to comment.