Skip to content

Commit

Permalink
audit: fix silent metatx merge in comp timelock (#212)
Browse files Browse the repository at this point in the history
* audit: fix silent metatx merge in comp timelock

* chore: use uint256 insetad of uint

---------

Co-authored-by: Orland0x <[email protected]>
  • Loading branch information
pscott and Orland0x authored Jun 20, 2023
1 parent 4027118 commit 6db831d
Show file tree
Hide file tree
Showing 7 changed files with 41 additions and 5 deletions.
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

0 comments on commit 6db831d

Please sign in to comment.