Modexp has arbitrary gas limit
Description
The Secp256r1 library makes use of the EIP-198↗ precompile in order to do modular exponentiation. This function is located at address 0x5
and is called near the end of the function.
function modexp(
uint _base,
uint _exp,
uint _mod
) internal view returns (uint ret) {
// bigModExp(_base, _exp, _mod);
assembly {
if gt(_base, _mod) {
_base := mod(_base, _mod)
}
// Free memory pointer is always stored at 0x40
let freemem := mload(0x40)
mstore(freemem, 0x20)
mstore(add(freemem, 0x20), 0x20)
mstore(add(freemem, 0x40), 0x20)
mstore(add(freemem, 0x60), _base)
mstore(add(freemem, 0x80), _exp)
mstore(add(freemem, 0xa0), _mod)
let success := staticcall(1500, 0x5, freemem, 0xc0, freemem, 0x20)
switch success
case 0 {
revert(0x0, 0x0)
}
default {
ret := mload(freemem)
}
}
}
A gas limit of 1,500 is set for this operation. After EIP-2565↗, the precompile was updated to become more optimized and cost less gas. Before this optimization, the gas cost was sometimes very high and often overestimated. The EIP provides a function to calculate the approximate gas cost, and using the parameters from the library, we calculated it to be around 1,360 gas, which is barely within the limit. With EIP-198 pricing, the cost was significantly higher.
Some chains have not yet implemented this optimization --- one example being the BNB chain, which plans to implement their equivalent BEP-225 around August 30th, 2023.
The standard for signature validation methods, EIP-1271↗, also states the following:
Since there [is] no gas-limit expected for calling the
isValidSignature()
function, it is possible that some implementation will consume a large amount of gas. It is therefore important to not hardcode an amount of gas sent when calling this method on an external contract as it could prevent the validation of certain signatures.
Impact
On chains without the gas optimization change for the precompile, the contract will either not work or randomly work for certain keys and signatures but not others. In the worst-case scenario, someone could be extremely lucky and manage to transfer money in but not be able to get them out again. The main risk is just the functionality of the module being broken.
Recommendations
Provide more or the maximum amount of gas to this function call:
let success := staticcall(not(0), 0x5, freemem, 0xc0, freemem, 0x20)
Remediation
This issue has been acknowledged by Biconomy Labs, and a fix was implemented in commit 5c5a6bfe↗.