Basic Powder Meerkat
High
As mentioned in the README
, the project will be deployed at:
Ethereum, Arbitrum, Rari Chain, zkSync Mainnet, Base, Polygon, OP Mainnet
The GovernanceStakerOnBehalf.sol
contract includes several functions (stakeOnBehalf
, stakeMoreOnBehalf
, alterDelegateeOnBehalf
, alterClaimerOnBehalf
, withdrawOnBehalf
, and claimRewardOnBehalf
) that rely on EIP-712 signatures for authorization. However, the hash computation for these functions does not include the chainId
parameter, which is a critical component of the EIP-712 domain separator. This omission makes it possible for an attacker to replay valid signatures on different chains.
stakeOnBehalf
, stakeMoreOnBehalf
, alterDelegateeOnBehalf
, alterClaimerOnBehalf
, withdrawOnBehalf
, and claimRewardOnBehalf
functions verify the authenticity of transactions using EIP-712 signatures. However, the _hashTypedDataV4()
does not include the chainId
in the hash computation, making it possible for an attacker to replay the same transaction on different chains.
This can happen when there is also a fork in the chain.
For example stakeOnBehalf()
functions works the following way:
function stakeOnBehalf(
uint256 _amount,
address _delegatee,
address _claimer,
address _depositor,
uint256 _deadline,
bytes memory _signature
) external virtual returns (DepositIdentifier _depositId) {
_revertIfPastDeadline(_deadline);
_revertIfSignatureIsNotValidNow(
_depositor,
_hashTypedDataV4(
keccak256(
abi.encode(
STAKE_TYPEHASH,
_amount,
_delegatee,
_claimer,
_depositor,
_useNonce(_depositor),
_deadline
)
)
),
_signature
);
_depositId = _stake(_depositor, _amount, _delegatee, _claimer);
}
An attacker can exploit this vulnerability by monitoring transactions on one chain and replaying them on another chain(fork) where the same contract is deployed. This can lead to unauthorized transactions on the target chain, potentially leading to a loss of staked tokens, misappropriation of rewards, and delegation changes.
Manual Review
Include chainId
explicitly in the hash computation for all affected functions, e.g.
bytes32 public constant STAKE_TYPEHASH = keccak256(
"Stake(uint256 amount,address delegatee,address claimer,address depositor,uint256 chainId,uint256 nonce,uint256 deadline)"
);
function stakeOnBehalf(
uint256 _amount,
address _delegatee,
address _claimer,
address _depositor,
uint256 _deadline,
bytes memory _signature
) external virtual returns (DepositIdentifier _depositId) {
_revertIfPastDeadline(_deadline);
_revertIfSignatureIsNotValidNow(
_depositor,
_hashTypedDataV4(
keccak256(
abi.encode(
STAKE_TYPEHASH,
_amount,
_delegatee,
_claimer,
_depositor,
block.chainid,
_useNonce(_depositor),
_deadline
)
)
),
_signature
);
_depositId = _stake(_depositor, _amount, _delegatee, _claimer);
}