The withdrawContributions function in Migration.sol takes any vault as input. As long as the vault is valid and has an inactive buyout, a user may call withdrawContributions even if the proposal they contributed to is LIVE.
This may lead to users not being able to commit their proposals if the Migrations.sol contract does not contain enough ETH for their commits.
A malicious user may also swap their vault tokens for any other vault tokens in the Migration.sol contract since withdrawContributions sends the tokens of the vault given as input. So if a user joined a proposal with 100 tokens of vault A and calls withdrawContributions with _vault set to vault B, the user will receive 100 vault B tokens.
Proof of Concept
Scenario 1: Malicious user withdraws ETH so that other proposals may not be commited
A user joins a proposal for vault A with 1 ETH, which is then commited and the ETH is sent to Buyout.sol.
Another user joins a proposal for vault B with 1 ETH, which is not yet commited so the ETH stays in Migration.sol.
The first user calls withdrawContributions with the proposalId they called join with and the vault set to vault B (it could be any valid vault).
The withdrawContributions will not revert since the buyout state for vault B is INACTIVE (assuming no one starts a buyout) and the '''migrationInfo[_vault][_proposalId].newVault``` should return a 0 address since the proposalId is valid only for vault A due to the incrementing of the proposalId.
Since the userProposalEth returns 1 ETH, the first user will receive 1 ETH and Migration.sol will lose 1 ETH. Migration.sol now has 0 ETH.
The second user will now not be able to commit their proposal since the Migration.sol contract contains less ETH than the totalEth returned by migrationInfo for that proposalId. Migration.sol does not have enough ETH for the user to call commit.
Scenario 2: Malicious user swaps worthless vault A tokens for valuable vault B tokens
A malicious user joins a proposal for vault A with 1 ETH and 100 vault A tokens.
A proposal for vault B has 100 vault B tokens and is not commited yet.
The proposal for vault A is commited, sending totalEth and totalFractions of the proposal for vault A to the Buyout.sol contract.
The malicious user calls withdrawContributions with _vault set to vault B and using the vault A proposalId.
The withdrawContributions will not revert since the buyout state for vault B is INACTIVE (assuming no one starts a buyout) and the '''migrationInfo[_vault][_proposalId].newVault``` should return a 0 address since the proposalId is valid only for vault A due to the incrementing of the proposalId.
The userProposalFractions for the malicious user will return 100 since they joined the vault A proposal with 100 tokens.
Migration.sol will send the vault B tokens to the malicious user instead of the correct vault A tokens. (Assuming the contract has enough ETH to send to the malicious user as well)
Recommended Mitigation Steps
Since no 2 vaults will have the same proposalId, check that the migrationInfo does not return an empty proposal in withdrawContributions.
Lines of code
https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L292-L326
Vulnerability details
Impact
The
withdrawContributions
function inMigration.sol
takes any vault as input. As long as the vault is valid and has an inactive buyout, a user may callwithdrawContributions
even if the proposal they contributed to isLIVE
.This may lead to users not being able to commit their proposals if the
Migrations.sol
contract does not contain enough ETH for their commits.A malicious user may also swap their vault tokens for any other vault tokens in the
Migration.sol
contract sincewithdrawContributions
sends the tokens of the vault given as input. So if a user joined a proposal with 100 tokens of vault A and callswithdrawContributions
with _vault set to vault B, the user will receive 100 vault B tokens.Proof of Concept
Scenario 1: Malicious user withdraws ETH so that other proposals may not be commited
A user joins a proposal for vault A with 1 ETH, which is then commited and the ETH is sent to
Buyout.sol
.Another user joins a proposal for vault B with 1 ETH, which is not yet commited so the ETH stays in
Migration.sol
.The first user calls
withdrawContributions
with the proposalId they calledjoin
with and the vault set to vault B (it could be any valid vault).The
withdrawContributions
will not revert since the buyout state for vault B is INACTIVE (assuming no one starts a buyout) and the '''migrationInfo[_vault][_proposalId].newVault``` should return a 0 address since the proposalId is valid only for vault A due to the incrementing of the proposalId.Since the
userProposalEth
returns 1 ETH, the first user will receive 1 ETH andMigration.sol
will lose 1 ETH.Migration.sol
now has 0 ETH.The second user will now not be able to commit their proposal since the
Migration.sol
contract contains less ETH than the totalEth returned by migrationInfo for that proposalId.Migration.sol
does not have enough ETH for the user to call commit.Scenario 2: Malicious user swaps worthless vault A tokens for valuable vault B tokens
A malicious user joins a proposal for vault A with 1 ETH and 100 vault A tokens.
A proposal for vault B has 100 vault B tokens and is not commited yet.
The proposal for vault A is commited, sending totalEth and totalFractions of the proposal for vault A to the
Buyout.sol
contract.The malicious user calls
withdrawContributions
with _vault set to vault B and using the vault A proposalId.The
withdrawContributions
will not revert since the buyout state for vault B is INACTIVE (assuming no one starts a buyout) and the '''migrationInfo[_vault][_proposalId].newVault``` should return a 0 address since the proposalId is valid only for vault A due to the incrementing of the proposalId.The userProposalFractions for the malicious user will return 100 since they joined the vault A proposal with 100 tokens.
Migration.sol
will send the vault B tokens to the malicious user instead of the correct vault A tokens. (Assuming the contract has enough ETH to send to the malicious user as well)Recommended Mitigation Steps
Since no 2 vaults will have the same proposalId, check that the migrationInfo does not return an empty proposal in
withdrawContributions
.