-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(forge-lint): [claude] unchecked calls #10810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, I think that's really great. Left couple of comments, pls check
} | ||
} | ||
|
||
/// Checks if an expression is an ERC20 `transfer` or `transferFrom` call. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I suggest moving function names in fn description and simplify check from match to bellow
/// Checks if an expression is an ERC20 `transfer` or `transferFrom` call.
/// `function ERC20.transfer(to, amount)`
// `function ERC20.transferFrom(from, to, amount)`
///
/// Validates both the method name and argument count to avoid false positives
/// from other functions that happen to be named "transfer".
fn is_erc20_transfer_call(expr: &Expr<'_>) -> bool {
if let ExprKind::Call(call_expr, args) = &expr.kind {
// Must be a member access pattern: `token.transfer(...)`
if let ExprKind::Member(_, member) = &call_expr.kind {
let method_name = member.as_str();
return (args.len() == 2 && method_name == "transfer") ||
(args.len() == 3 && method_name == "transferFrom")
}
}
false
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, couldn't this give false warnings in case of a non ERC20 contract (Proxy) with transfer()
or transferFrom()
fns, e.g.
contract Proxy {
function transfer(address to, uint256 amount) public {
require(IERC20(token).transfer(to, amount), "Transfer failed");
}
}
contract MyContract {
Proxy public proxy;
function refill() public {
// this one fails lint
proxy.transfer(address(1), 1);
}
}
so maybe we should check that fn returns a bool? (even so it can flag false warnings but could be more robust)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mmm that's true.
this check is more tricky, let me think alternatives do we have
Use cleaner pattern matching and `solar_interface::kw` keywords instead of string literals. Move function signatures to doc comments.
prompt to address @grandizzy's feedback:
|
Motivation
forge-lint
Solution
Add 2 new lints to warn about unchecked:
Setup:
The development of this PR was done by running Claude code and giving it access to both solar (so that it could check the AST impls when necessary) and foundry:
IMPORTANT: link
@foundry/docs/dev/lintrules.md
in the prompts, so that claude is aware of the implementation and testing guidelines forforge-lint
Used Prompts:
Each individual prompt is separated by a blank line, to showcase the conversation with Claude code.
at this point we had a working lint! but it was quite ugly, so i made a follow-up prompt to improve it:
here we had a working lint for unchecked ERC20 transfers!
however, i wanted to push it a little bit more and also asked for a new lint that would do the same for low-level calls
it correctly implemented the logic to identify low-level calls.
although, as expected, it only checked for calls without variable assignments. Since low-level calls return a tuple, it is also necessary to handle the case where users do something with the returned bytes but not with the success boolean.
at this point we had a working implementation, which i tweaked a little bit manually. Finally i asked it to refactor the code and merge everything into a single file to keep "unchecked call" lints together:
finally i did a style-based refactor for code readability and consistency (and also had to run clippy and fmt)