-
Notifications
You must be signed in to change notification settings - Fork 25
Warn if contract has a selfdestruct call #347
Conversation
I'm working on the test fails now. |
I confirmed tests are passing locally this time ;)
for consistency.
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.
Thanks for the contribution @HardlyDifficult! Could you add some tests to check that the feature works properly?
Also, I don't think the current method for scanning the AST manages to catch all cases, since it only scans the topmost set of statements in a function body. So, if there is a selfdestruct
statement within a nested block, it will miss it. For instance:
contract Destructible {
function hasDestruct() {
if (true) {
if (true) {
if (true) {
selfdestruct(address(0));
}
}
}
}
}
I think it'd be more robust to use a visitor pattern to walk the AST, and check for selfdestruct
nodes anywhere in the tree.
Also, keep in mind that the selfdestruct
call may be present in an ancestor, so you'd need to repeat this process for all contracts in the hierarchy. But I'd suggest to start from just the specific contract, and then work our way up!
…erstood the structure of the AST. I made the changes you suggested and added a Test. Please let me know if this looks okay. I'm unsure how to address ancestor concern. I wrote a test for this, and commented it out because it fails ATM. If you have a suggestion on how to proceed, I'm happy to take a crack at it.
@spalladino Correct, the original approach did not work. I had misunderstood the structure of the AST. I made the changes you suggested and added a Test. Please let me know if this looks okay. I'm unsure how to address ancestor concern. I wrote a test for this, and commented it out because it fails ATM. If you have a suggestion on how to proceed, I'm happy to take a crack at it. |
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.
Looks much better @HardlyDifficult, thanks!! I think the hasKeyValue
approach can be improved using an actual AST visitor, but I couldn't find any existing implementation for the format returned by solc.
As for the parent implementations, note that the AST returns a contractDependencies
node, with node IDs that refer to parent contracts you'd need to scan as well.
test/scripts/add.test.js
Outdated
@@ -92,10 +92,26 @@ contract('add script', function() { | |||
this.logs.errors[0].should.match(/constructor/i); | |||
}); | |||
|
|||
it('should not warn when adding a contract without a constructor', async function() { | |||
it('should not warn when adding a contract without a constructor or a selfdestruct cull', async function() { |
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.
typo: cull => call
|
||
hasKeyValue(data, key, value) { | ||
if (!data) return false | ||
if (data[key] === value) return true |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
if (typeof(data) === 'object') { | ||
for (const child_key in data) { | ||
const child_value = data[child_key]; | ||
if (this.hasKeyValue(child_value, key, value)) { |
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.
I think the type check should be moved onto here: only call this.hasKeyValue
if child_value
is an object.
if (!data) return false | ||
if (data[key] === value) return true | ||
if (typeof(data) === 'object') { | ||
for (const child_key in data) { |
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.
Please make sure to use camelCase
for all variable names, and not snake_case
, for consistency
test/scripts/add.test.js
Outdated
}); | ||
|
||
// TODO: This warning is not yet implemented | ||
// it('should warn when adding a contract who\'s parent has a selfdestruct call', async function() { |
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.
Use it.skip
instead of commenting out the code if it's still unimplemented
I made changes addressing the code review feedback from @spalladino. For the parent implementations, I'm not sure how to go from node ID to the AST. I coded an alternative which is similar. Per the unit test it seems to work. Thoughts on this?
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.
@HardlyDifficult the new approach for querying ancestors looks good! I added a minor comment regarding style, to keep consistency with the rest of the codebase.
Could you look into whether it's possible to detect DELEGATECALL
s with the same hasKeyValue
method, as @nventuro suggested, so we output another warning for the same reason? (ie the contract may delegatecall into an arbitrary one that does selfdestruct)
const ast = fs.parseJson(contractDataPath).ast | ||
if (this.hasKeyValue(ast, "typeIdentifier", "t_function_selfdestruct_nonpayable$_t_address_$returns$__$")) return true | ||
for (let i = 0; i < ast.nodes.length; i++) { | ||
for (let j = 0; j < (ast.nodes.baseContracts || []).length; j++) { |
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.
RE delegate call: happy too. I looked at this a bit but couldn't figure out how to use delegate call (never tried this in a contract before). Could you provide am example I can test with? I tried the following but it does not work (no error, but it does not actually selfdestruct the contract): this.delegatecall(bytes4(keccak256("selfdestruct(uint256)")), msg.sender); thanks, |
A |
Delegate call does not seem to create a clear entry in the AST we could scan for. A couple options I can think of:
Either of these seem okay? |
@HardlyDifficult I made a small example, and the AST seems to record the delegate call with the type @nventuro or @ajsantander, given that you are working with the Solidity compiler internals, could you confirm if this is always the case for a delegatecall (at least when not using assembly)? I think we are getting real close to wrapping this up!! |
Including a unit test for that warning. RE feedback: Changed the for loop style to for-of as suggested. I considered adding a warning for callcode as well, but there is already a message "Warning: "callcode" has been deprecated in favour of "delegatecall"."
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.
Looks good @HardlyDifficult, thanks for the update! There is a test missing (the contract was added, but not the corresponding test case), and the CI is reporting a max call stack error (maybe hasTypeIdentifier
is looping over the same baseContract or node over and over again?).
this.logs.warns[0].should.match(/selfdestruct/i); | ||
}); | ||
|
||
it('should warn when adding a contract with a delegatecall call', async function() { |
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.
Please add a test for WithParentWithDelegateCall
// Log a warning anytime `delegatecall` is found. This is a potential security risk, | ||
// but not an error/throw as it may be a desired feature | ||
if (this.hasDelegateCall(Contracts.getLocalPath(contractName))) { | ||
log.warn(`Contract ${contractName} (or its parent class) has a delegatecall call. Avoid using low-level function 'delegatecall'. This is potentially a security risk. Please review and consider removing this 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.
Let's change:
Avoid using low-level function 'delegatecall'. This is potentially a security risk.
To the following:
This is potentially a security risk, as the logic contract could be destructed by issuing a delegatecall to another contract with a selfdestruct instruction.
WDYT?
test/scripts/add.test.js
Outdated
this.logs.warns[0].should.match(/selfdestruct/i); | ||
}); | ||
|
||
it('should warn when adding a contract who\'s parent has a selfdestruct call', async function() { |
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.
who' s => whose (sorry, grammar nazi here)
Updating per the latest feedback. Note that I expect the test will still fail. I'm having trouble locally... When running the test suite, it simply skips the failing tests without message. ".... parseInit √ should not init √ should init with default when init is set √ should init when args is set √ should init with specific function √ should init with specific function and args Contract: add-all script E:\zos-cli2>" And when I try to run the test explicitly it also just terminates without messaging why.. "E:\zos-cli2>C:\Users\lopop\AppData\Roaming\npm\truffle test .\test\scripts\add-all.test.js Using network 'test'. Contract: add-all script E:\zos-cli2>" I tried one-off local tests (for all 4 test cases) and it seems to work fine. "E:\zostest>node ..\zos-cli2\lib\bin\zos-cli add SelfDestruct Compiling contracts Compiling .\contracts\Child.sol... Compiling .\contracts\Nested2.sol... Compiling .\contracts\SelfDestruct.sol... Writing artifacts to .\build\contracts Adding SelfDestruct Contract SelfDestruct (or its parent class) has a selfdestruct call. This is potentially a security risk. Please review and consider removing this call. Successfully written zos.json" I can't repro the fail. I'm unsure how to proceed at the moment. I'm going to set this down and try again later, let me know if you have any tips. thanks.
Quick update: I have the stack overflow fixed locally, but currently investigating another issue. (the AST is referencing every class in the .sol file, vs just ones related to the contract being tested... leading to false positives) Will continue tomorrow. |
And addressed the feedback from @spalladino
I believe all the outstanding feedback has been addressed. Let me know if there's anything I missed. thanks, |
Awesome @HardlyDifficult, thanks for the hard work!! I'll port the commit to the new monorepo at zeppelinos/zos. |
Signed-off-by: Santiago Palladino <[email protected]>
Fix for #320
We are scanning the AST as suggested by @nventuro