DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: medium
Invalid

Lack of chainID validation allows reuse of blueprint across forks

Summary

The LibTracker Faucet uses C.sol contract in which legacy chainId and chainId variables are declared which are used inside Libtracker when determining chainId for the calculation of domain Separator . However , the ChainId is not determined by block.chainId rather hardcoded which exposes blueprints to be re-used across different forks of eth mainnet in the event of chain fork. This breaks the core part of the system assumption that one signature should be used only once which is valid.

Vulnerability Details

Inside TrackerFaucet.sol, the modifier verifyRequisition uses LibTractor._getBlueprintHash to determine the blue print hash.
and check if the blue print publisher is infact the one who has signed it .

modifier verifyRequisition(LibTractor.Requisition calldata requisition) {
bytes32 blueprintHash = LibTractor._getBlueprintHash(requisition.blueprint);
// snip
_;
}

this is used across publishRequisition and cancelBluePrint methods to ensure authenticity of blueprints.

The LibTractor._getBlueprintHash is defined as follows

function _getBlueprintHash(Blueprint calldata blueprint) internal view returns (bytes32) {
return
_hashTypedDataV4(
keccak256(
abi.encode(
BLUEPRINT_TYPE_HASH,
blueprint.publisher,
keccak256(blueprint.data),
keccak256(abi.encodePacked(blueprint.operatorPasteInstrs)),
blueprint.maxNonce,
blueprint.startTime,
blueprint.endTime
)
)
);
}

the method _hashTypedDataV4 is defined as

function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), structHash));
}
/**
* @notice Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
return
keccak256(
abi.encode(
BLUEPRINT_TYPE_HASH,
TRACTOR_HASHED_NAME,
TRACTOR_HASHED_VERSION,
C.getChainId(),
address(this)
)
);
}

if we check following line C.getChainId(), , this line , According to EIP712 (uint256 chainId the EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain.)

intends to fetch the chain id of the current active chain.

However , if we look at C.getChainId(), method ,

function getChainId() internal pure returns (uint256) {
return CHAIN_ID;
}

This is just returning the hardcoded value uint256 private constant CHAIN_ID = 1;.

If a fork of Ethereum is
made after the contract’s creation, every blue print will be usable in both forks.

Exploit scenario

Bob has submitted a signature blueprint to perform some beanstalk operation and allows Eve to do it ( let's say beanstalk allows operator and owner paradigm for managing some operations involving tokens ) using that blueprint . And imagine something catastrophic happens or ethereum community wants to introduce a big change,
let's name it London fork. When the London hard fork is executed, a subset of
the community declines to implement the upgrade. As a result, there are two parallel
chains with the same chainID value as 1 because it was hardcoded , and Eve can use Bob’s signature to transfer funds on both
chains because the domain separator does not use fresh block.chainId in its calculation.

Impact

Signature Replay of BluePrints across chains

Tools Used

Manual Review

Recommendations

Use block.chainId to determine the currently active chainId. This also adheres to EIP712's description of chainId param.

Reference

Check a similar finding by some fellows at ToB

https://solodit.xyz/issues/risk-of-reuse-of-signatures-across-forks-due-to-lack-of-chainid-validation-trailofbits-looksrare-pdf

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

Replay attack in case of hard fork - Hardcoded chainId 712

Support

FAQs

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