Skip to content
Merged
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
56 changes: 34 additions & 22 deletions status-network-contracts/src/rln/RLN.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,19 @@ contract RLN is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
}
}

/// @dev Computes the slash commitment key with the slasher address and hash.
/// The slasher address is included to ensure uniqueness per slasher, not allowing anyone else to override the same
/// hash without even knowing the private key.
/// @param sender: address of the slasher;
/// @param hash: keccak256 hash of abi.encodePacked(privateKey, rewardRecipient);
/// @return bytes32: the computed commitment key.
function _slashCommitmentKey(address sender, bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(sender, hash));
}

/// @dev Sets the slash reveal window time.
/// @param _slashRevealWindowTime: new reveal window time in seconds.
/// @notice The window time must be at least 1 second and no more than 365 days.
/// @notice The window time must be at least 1 second and no more than 1 day.
Copy link
Member

Choose a reason for hiding this comment

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

Can we reference the issue that it will close?

/// A non-zero value is required to ensure the queuing mechanism functions correctly.
/// An excessively large value could lock commitments indefinitely.
function setSlashRevealWindowTime(uint256 _slashRevealWindowTime) external onlyRole(DEFAULT_ADMIN_ROLE) {
Expand Down Expand Up @@ -154,23 +164,6 @@ contract RLN is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
}
}

/// @dev Slashes identity with privateKey.
/// @param privateKey: RLN private key as bytes32;
/// @param rewardRecipient: Address that will receive the slash reward;
function slash(bytes32 privateKey, address rewardRecipient) public onlyRole(SLASHER_ROLE) {
// Hash the private key using Poseidon to get identityCommitment
uint256 identityCommitment = poseidonHasher.hash(uint256(privateKey));

User memory member = members[identityCommitment];
if (member.userAddress == address(0)) {
revert RLN__MemberNotFound();
}
karma.slash(member.userAddress, rewardRecipient);
delete members[identityCommitment];

emit MemberSlashed(member.index, msg.sender);
}

/// @dev Commits to a future slash operation using a hash.
/// @notice This is the first step of the commit-reveal scheme for slashing.
/// The slasher must first commit to a hash of (privateKey, rewardRecipient) before revealing
Expand All @@ -190,7 +183,8 @@ contract RLN is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
revealStartTime = lastReveal + slashRevealWindowTime;
}

slashCommitments[account][hash] = revealStartTime;
bytes32 key = _slashCommitmentKey(msg.sender, hash);
slashCommitments[account][key] = revealStartTime;
lastRevealStartTime[account] = revealStartTime;
}

Expand All @@ -214,7 +208,8 @@ contract RLN is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
{
/// forge-lint: disable-next-line(asm-keccak256)
bytes32 hash = keccak256(abi.encodePacked(privateKey, rewardRecipient));
uint256 revealStartTime = slashCommitments[account][hash];
bytes32 key = _slashCommitmentKey(msg.sender, hash);
uint256 revealStartTime = slashCommitments[account][key];
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a fix, we should make it a fix commit. ideally it references the issue it closes


if (revealStartTime == 0) {
revert RLN__InvalidCommitment();
Expand All @@ -224,7 +219,24 @@ contract RLN is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
revert RLN__RevealWindowNotStarted();
}

delete slashCommitments[account][hash];
slash(privateKey, rewardRecipient);
delete slashCommitments[account][key];

uint256 identityCommitment = poseidonHasher.hash(uint256(privateKey));
User memory member = members[identityCommitment];
if (member.userAddress == address(0)) {
revert RLN__MemberNotFound();
}

// We make sure that the account slashed matches the account used during the commit phase
// otherwise someone could front-run the slasher by committing to slash a different account
// to skip the queuing mechanism.
if (account != member.userAddress) {
revert RLN__InvalidCommitment();
}

karma.slash(member.userAddress, rewardRecipient);
delete members[identityCommitment];

emit MemberSlashed(member.index, msg.sender);
Copy link
Member

Choose a reason for hiding this comment

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

Can we add a test for this please?

}
}
93 changes: 40 additions & 53 deletions status-network-contracts/test/RLN.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -171,54 +171,7 @@ contract RLNTest is Test {
vm.stopPrank();
}

/* ---------- SLASH ---------- */

function test_slash_succeeds() public {
uint256 distributorBalance = 50 ether;
vm.startPrank(owner);
karma.mint(address(distributor1), distributorBalance); // Mint Karma tokens to distributor1
distributor1.setUserKarmaShare(user2Addr, 10 ether);
vm.stopPrank();

// Register the identity first
vm.startPrank(registerAddr);
rln.register(identityCommitment1, user2Addr);
vm.stopPrank();

// Retrieve the assigned index
(, uint256 index1) = _memberData(identityCommitment1);

// burn event
vm.expectEmit(true, true, true, true);
emit IERC20Upgradeable.Transfer(user2Addr, address(0), 5 ether);

// reward mint event
vm.expectEmit(true, true, true, true);
emit IERC20Upgradeable.Transfer(address(0), rewardRecipientAddr, 0.5 ether);

// slash event
vm.expectEmit(true, true, true, true);
emit RLN.MemberSlashed(index1, slasherAddr);

vm.prank(slasherAddr);
rln.slash(privateKey1, rewardRecipientAddr);

// After slash, the member record should be cleared
(address u1, uint256 i1) = _memberData(identityCommitment1);
assertEq(u1, address(0));
assertEq(i1, 0);
}

function test_slash_fails_when_not_registered() public {
// Attempt to slash a non‐existent identity
vm.startPrank(slasherAddr);
vm.expectRevert(RLN.RLN__MemberNotFound.selector);
rln.slash(privateKey0, rewardRecipientAddr);
vm.stopPrank();
}

/* ---------- SLASH COMMIT/REVEAL ---------- */

function test_SlashCommitRevertsIfNoSlashRole() public {
bytes32 hash = keccak256(abi.encodePacked(privateKey0, rewardRecipientAddr));

Expand All @@ -237,6 +190,7 @@ contract RLNTest is Test {

function test_SlashCommitAddsNewHashWithSlashRole() public {
bytes32 hash = keccak256(abi.encodePacked(privateKey0, rewardRecipientAddr));
bytes32 key = keccak256(abi.encodePacked(slasherAddr, hash));

// Verify commitment doesn't exist yet
assertEq(rln.slashCommitments(user1Addr, hash), 0);
Expand All @@ -246,7 +200,7 @@ contract RLNTest is Test {
rln.slashCommit(user1Addr, hash);

// Verify commitment was added with a revealStartTime
assertGt(rln.slashCommitments(user1Addr, hash), 0);
assertGt(rln.slashCommitments(user1Addr, key), 0);

// Verify lastRevealStartTime was updated
assertGt(rln.lastRevealStartTime(user1Addr), 0);
Expand Down Expand Up @@ -285,11 +239,13 @@ contract RLNTest is Test {

// Commit the slash
bytes32 hash = keccak256(abi.encodePacked(privateKey0, rewardRecipientAddr));
bytes32 key = keccak256(abi.encodePacked(slasherAddr, hash));

vm.prank(slasherAddr);
rln.slashCommit(user1Addr, hash);

// Verify commitment exists with a revealStartTime
uint256 revealStartTime = rln.slashCommitments(user1Addr, hash);
uint256 revealStartTime = rln.slashCommitments(user1Addr, key);
assertGt(revealStartTime, 0);

// Warp time to allow reveal (skip to the reveal window)
Expand All @@ -312,7 +268,7 @@ contract RLNTest is Test {
rln.slashReveal(user1Addr, privateKey0, rewardRecipientAddr);

// Verify commitment was removed
assertEq(rln.slashCommitments(user1Addr, hash), 0);
assertEq(rln.slashCommitments(user1Addr, key), 0);

// Verify member was slashed
(address userAddress, uint256 userIndex) = _memberData(identityCommitment0);
Expand Down Expand Up @@ -343,21 +299,52 @@ contract RLNTest is Test {
rln.slashReveal(user1Addr, privateKey0, rewardRecipientAddr);
}

function test_SlashRevealRevertsIfAccountIsDifferentFromTheOneUsedDuringCommit() public {
vm.startPrank(owner);
karma.mint(user1Addr, 10 ether);
karma.mint(user2Addr, 10 ether);
vm.stopPrank();

vm.startPrank(registerAddr);
rln.register(identityCommitment1, user1Addr);
rln.register(identityCommitment2, user2Addr);
vm.stopPrank();

// commit slash for pk1 and user1
bytes32 hash1 = keccak256(abi.encodePacked(privateKey1, rewardRecipientAddr));
vm.prank(slasherAddr);
rln.slashCommit(user1Addr, hash1);

// malicious commit trying to slash pk1 using the empty queue of user2
vm.prank(slasherAddr);
rln.slashCommit(user2Addr, hash1);

// Attempt to reveal pk1 using queue for user2, so we skip the first commit in the queue
vm.expectRevert(RLN.RLN__InvalidCommitment.selector);
vm.prank(slasherAddr);
rln.slashReveal(user2Addr, privateKey1, rewardRecipientAddr);
}

function test_SlashCommitCreatesQueueForMultipleCommits() public {
// Commit three slashes for the same account
bytes32 hash1 = keccak256(abi.encodePacked(privateKey0, rewardRecipientAddr));
bytes32 key1 = keccak256(abi.encodePacked(slasherAddr, hash1));

bytes32 hash2 = keccak256(abi.encodePacked(privateKey1, rewardRecipientAddr));
bytes32 key2 = keccak256(abi.encodePacked(slasherAddr, hash2));

bytes32 hash3 = keccak256(abi.encodePacked(privateKey2, rewardRecipientAddr));
bytes32 key3 = keccak256(abi.encodePacked(slasherAddr, hash3));

vm.startPrank(slasherAddr);
rln.slashCommit(user1Addr, hash1);
uint256 revealTime1 = rln.slashCommitments(user1Addr, hash1);
uint256 revealTime1 = rln.slashCommitments(user1Addr, key1);

rln.slashCommit(user1Addr, hash2);
uint256 revealTime2 = rln.slashCommitments(user1Addr, hash2);
uint256 revealTime2 = rln.slashCommitments(user1Addr, key2);

rln.slashCommit(user1Addr, hash3);
uint256 revealTime3 = rln.slashCommitments(user1Addr, hash3);
uint256 revealTime3 = rln.slashCommitments(user1Addr, key3);
vm.stopPrank();

// Verify that each subsequent commit has a later reveal time
Expand Down
Loading