Platform: Code4rena
Start Date: 13/10/2023
Pot Size: $31,250 USDC
Total HM: 4
Participants: 51
Period: 7 days
Judge: 0xsomeone
Id: 295
League: ETH
Rank: 30/51
Findings: 1
Award: $23.96
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: immeas
Also found by: Alra, Arz, GREY-HAWK-REACH, alexzoid, radev_sw, rvierdiiev, sorrynotsorry
23.9577 USDC - $23.96
Each subaccount should have policy that was provided for them by main console. So every time, when subaccount owners want to execute any transaction, then SafeModerator
will check tx. It will validate if tx doesn't violate it's policy. So PolicyValidator.isPolicySignatureValid
function will be called to do that check.
function isPolicySignatureValid(address account, bytes32 transactionStructHash, bytes calldata signatures) public view returns (bool) { // Get policy hash from registry bytes32 policyHash = PolicyRegistry(AddressProviderService._getRegistry(_POLICY_REGISTRY_HASH)).commitments(account); if (policyHash == bytes32(0)) { revert NoPolicyCommit(); } // Get expiry epoch and validator signature from signatures (uint32 expiryEpoch, bytes memory validatorSignature) = _decompileSignatures(signatures); // Ensure transaction has not expired if (expiryEpoch < uint32(block.timestamp)) { revert TxnExpired(expiryEpoch); } // Build validation struct hash bytes32 validationStructHash = TypeHashHelper._buildValidationStructHash( TypeHashHelper.Validation({ transactionStructHash: transactionStructHash, policyHash: policyHash, expiryEpoch: expiryEpoch }) ); // Build EIP712 digest with validation struct hash bytes32 txnValidityDigest = _hashTypedData(validationStructHash); address trustedValidator = AddressProviderService._getAuthorizedAddress(_TRUSTED_VALIDATOR_HASH); // Empty Signature check for EOA signer if (trustedValidator.code.length == 0 && validatorSignature.length == 0) { // TrustedValidator is an EOA and no trustedValidator signature is provided revert InvalidSignature(); } // Validate signature return SignatureCheckerLib.isValidSignatureNow(trustedValidator, txnValidityDigest, validatorSignature); }
What this function does is actually the check that offchain trusted validator indeed has allowed(signed) subaccount to execute such transaction. Also it checks that this approve is not old: expiryEpoch < uint32(block.timestamp)
, which means that validator will sign messages for some period of time. When signature is old, then transaction can't be executed.
For each subaccount ConsoleFallbackHandler
is set as fallback at the deployment. It has isValidSignature
function in order to implement EIP-1271.
Let's check how it works. The function calls another version of isValidSignature
function.
https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/ConsoleFallbackHandler.sol#L39-L55
function isValidSignature(bytes memory _data, bytes memory _signature) public view override returns (bytes4) { // Caller should be a Safe GnosisSafe safe = GnosisSafe(payable(msg.sender)); bytes32 messageHash = getMessageHashForSafe(safe, _data); if (_signature.length == 0) { require(safe.signedMessages(messageHash) != 0, "Hash not approved"); } else { /// @dev For validating signatures, `PolicyValidator` is invoked to ensure that /// the intent of `messageHash` signed by the owners of the safe, complies with its committed /// policy PolicyValidator policyValidator = PolicyValidator(AddressProviderService._getAuthorizedAddress(_POLICY_VALIDATOR_HASH)); require(policyValidator.isPolicySignatureValid(msg.sender, messageHash, _signature), "Policy not approved"); safe.checkSignatures(messageHash, _data, _signature); } return EIP1271_MAGIC_VALUE; }
So the purpose of this function is to check if signature is valid. How it can be used? For example subaccount is an owner in another Safe wallet, so on signature validation of tx from that wallet, isValidSignature
function of subaccount will be called. In this case, subaccount also should only be able to provide signatures for txs that doesn't violate their's policy. That's why ConsoleFallbackHandler.isValidSignature
has policy validation again. So ConsoleFallbackHandler
expects, that when signatures will be checked, then it should pass policy validation at the moment of check.
But there is another way to make signature valid. This way is to use SignMessageLib
contract and store your message into the signedMessages
. If such message is stored then there will be no check in the isValidSignature
function.
Such ability allows owners of subaccount to bypass validation sometimes. For example, as i have described before each validator signature has expiring timestamp, which means that signature should be valid only some time. But in case if owner will store this message to signedMessages
then on the time of execution will be no policy validation and expiration will not be checked.
Another thing that can happen is that policy will be changed for the subaccount. And after that all messages stored in the signedMessages
will be still valid.
I don't know exactly how validator will work, but it's possible for owners of subaccount to store some messages into signedMessages
that they would like to be used by other Safe wallets just before their policy will be chnaged. As result all those signatures will work against their policy.
Subaccounts may provide signatures that violate their policy.
VsCode
If think about expiration time check, then i would remove signedMessages
from isValidSignature
validation. So it's possible only to use fresh signatures.
If think about changed policy for subaccount only, then i would remove all signedMessages
from subaccount, when policy is changed. So this move can be executed as multi send from console safe: first remove all signedMessages
and then change policy for subaccount.
Error
#0 - c4-pre-sort
2023-10-21T04:06:00Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-21T04:06:04Z
raymondfam marked the issue as primary issue
#2 - 0xad1onchain
2023-10-25T14:33:28Z
This is known and already accounted for. To store a signed message, console owners or in case of subaccount, the operators need to execute a transaction from the safe to save a signature in presigned messages, and that transaction like any other transaction would go through policy validator
#3 - c4-sponsor
2023-10-25T14:33:30Z
0xad1onchain (sponsor) disputed
#4 - alex-ppg
2023-10-27T22:05:36Z
As the Sponsor has specified, the policy validation check has been removed from the stored message flow as a matter of optimization as the stored hash would have already been validated by a policy one way or the other.
The Warden does articulate that there is no expiry associated with stored messages that have been signed which is correct, however, this is a QA report (Low) as it would still require the policy signer to allow the behavior in the first place. The altered policy avenue is also interesting and something that the Sponsor may wish to handle.
I consider this exhibit thus sufficient but as a QA rather than a Medium.
#5 - c4-judge
2023-10-27T22:05:42Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-10-27T22:05:55Z
alex-ppg changed the severity to QA (Quality Assurance)
#7 - c4-judge
2023-10-31T21:31:13Z
alex-ppg marked the issue as grade-b