Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

audit: fix silent metatx merge in comp timelock #212

Merged
merged 3 commits into from
Jun 20, 2023
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
2 changes: 1 addition & 1 deletion .forge-snapshots/ProposeSigComp.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
148841
148980
2 changes: 1 addition & 1 deletion .forge-snapshots/VoteSigComp.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
50322
50452
2 changes: 1 addition & 1 deletion .forge-snapshots/VoteSigCompMetadata.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
51865
51995
2 changes: 1 addition & 1 deletion .forge-snapshots/VoteTxComp.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
43892
44022
2 changes: 1 addition & 1 deletion .forge-snapshots/VoteTxCompMetadata.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
45476
45606
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ contract CompTimelockCompatibleExecutionStrategy is SimpleQuorumExecutionStrateg
error ProposalNotQueued();
/// @notice Thrown if the proposal execution payload hash is already queued.
error DuplicateExecutionPayloadHash();
/// @notice Thrown if the same MetaTransaction appears twice in the same payload (salt is not taken into account).
error DuplicateMetaTransaction();
/// @notice Thrown if veto caller is not the veto guardian.
error OnlyVetoGuardian();
/// @notice Thrown if the transaction is invalid.
Expand Down Expand Up @@ -103,11 +105,26 @@ contract CompTimelockCompatibleExecutionStrategy is SimpleQuorumExecutionStrateg
proposalExecutionTime[proposal.executionPayloadHash] = executionTime;

MetaTransaction[] memory transactions = abi.decode(payload, (MetaTransaction[]));

// An array to check that there are no duplicates in the list of transactions.
// We must do this because the Compound Timelock will silently "merge" two duplicate transactions.
// This could be problematic for off-chain indexers and UI tools.
bytes32[] memory txHashes = new bytes32[](transactions.length);

for (uint256 i = 0; i < transactions.length; i++) {
// Comp Timelock does not support delegate calls.
if (transactions[i].operation == Enum.Operation.DelegateCall) {
revert InvalidTransaction();
}

// Check there are not duplicates.
// Do not include `executionTime` because it's a constant.
bytes32 txHash = keccak256(abi.encode(transactions[i].to, transactions[i].value, "", transactions[i].data));
for (uint256 j = 0; j < i; j++) {
if (txHashes[j] == txHash) revert DuplicateMetaTransaction();
}
txHashes[i] = txHash;

timelock.queueTransaction(
transactions[i].to,
transactions[i].value,
Expand Down
19 changes: 19 additions & 0 deletions test/CompTimelockExecutionStrategy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ abstract contract CompTimelockExecutionStrategyTest is SpaceTest {
error TimelockDelayNotMet();
error ProposalNotQueued();
error DuplicateExecutionPayloadHash();
error DuplicateMetaTransaction();
error OnlyVetoGuardian();
error InvalidTransaction();
event CompTimelockCompatibleExecutionStrategySetUp(
Expand Down Expand Up @@ -97,6 +98,24 @@ abstract contract CompTimelockExecutionStrategyTest is SpaceTest {
space.execute(proposalId, abi.encode(transactions));
}

function testQueueingDuplicateMetaTransaction() external {
MetaTransaction[] memory transactions = new MetaTransaction[](2);
transactions[0] = MetaTransaction(recipient, 1, "", Enum.Operation.Call, 0);
// Salt differs but content does not
transactions[1] = MetaTransaction(recipient, 1, "", Enum.Operation.Call, 1);
uint256 proposalId = _createProposal(
author,
proposalMetadataURI,
Strategy(address(timelockExecutionStrategy), abi.encode(transactions)),
new bytes(0)
);
_vote(author, proposalId, Choice.For, userVotingStrategies, voteMetadataURI);
vm.roll(block.number + space.maxVotingDuration());

vm.expectRevert(DuplicateMetaTransaction.selector);
space.execute(proposalId, abi.encode(transactions));
}

function testQueueingRejectedProposal() external {
MetaTransaction[] memory transactions = new MetaTransaction[](1);
transactions[0] = MetaTransaction(recipient, 1, "", Enum.Operation.Call, 0);
Expand Down