
Start Building On EVM @evmchronicle_io 👈 EVM Storage Explorer @SolidityREPL 👈 Browser-based Solidity execution environment
Jakarta
Joined August 2014
- Tweets142
- Following147
- Followers205
- Likes179
Testing Solidity directly in browser with @SolidityREPL.
Import files from any public Git repository.
New exploit on Ethereum:
TrustedVolumes lost around $5.87M in WETH, USDT, WBTC, and USDC.
The exploit appears to involve a TrustedVolumes-controlled custom RFQ swap proxy:
0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756
Victim contract / resolver:
0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31
Exploiter:
0xC3EBDdEa4f69df717a8f5c89e7cF20C1c0389100
Trace exploit transaction:
evmchronicle.io/tx/0xc5c61b3…
See image below for the interesting things I found on this exploit.
Small details. Big security implications.
abi.encodePacked can create hash collisions when multiple dynamic values are packed together.
Example:
keccak256(abi.encodePacked(a, b));
If a and b are dynamic types like string or bytes, different inputs can produce the same packed bytes.
Example:
abi.encodePacked("ab", "c")
and:
abi.encodePacked("a", "bc")
both become:
"abc"
So their hash is the same.
The issue is not keccak256.
The issue is that packed encoding removes boundaries between dynamic values.
Use abi.encode(a, b) when boundaries matter.
A failed low-level .call does not automatically stop the function.
The process can still continue.
Example:
function withdraw(address payable a, uint256 amount) external {
(bool ok, ) = a.call{value: amount}(""); // call fail
doSomethingElse(); // can still run even if ok == false
}
This is one of the easiest Solidity patterns that can lead to a contract drain:
(bool ok, ) = msg.sender.call{value: amount}("");
balances[msg.sender] = 0;
Why this is deadly:
You send ETH first.
You clear balance after.
So if msg.sender is a contract, it can re-enter during the callback while its balance is still untouched.
Same balance.
Multiple withdrawals.
Possible full drain.
call is not the real mistake.
Calling out before fixing your internal state is.
A lot of Solidity code still treats extcodesize as if it were a security feature.
Checking extcodesize(addr) == 0 only tells you one thing: at that exact moment, the address has no deployed code.
Usually developer use this feature cause they wants to block smart contracts and allow only regular wallets.
The thinking is simple: "If the caller is not a contract, the function is safer."
That is where the mistake begins.
On the surface, it looks strict and intuitive: if the caller has no code, then it must be safe to treat it like a normal user. But that assumption breaks in one of the most important edge cases.
The most well-known issue is constructor bypass.
During contract construction, the contract's code is not yet stored on-chain. That means if a contract calls another contract from its constructor, extcodesize(address(this)) is still zero during that call.
So code like this:
require(msg.sender.code.length == 0, "no contracts");
can still be bypassed by a contract calling in from its constructor.
That alone should be enough to kill the idea that this is a reliable security boundary. The check looks strict, but it can fail in one of the most important edge cases.
This commonly used batching pattern can easily lead to vulnerabilities
Batching is a pattern many developers use to let users execute multiple actions in one transaction. On the surface, it looks simple and useful. But if it is added without fully understanding how it changes execution flow, it can quietly turn otherwise normal functions into vulnerable ones. The dangerous part is that the bug often does not come from batching alone, but from how it interacts with other contract logic.
A simplified version of how batching works looks like this:
function batch(bytes[] calldata calls, bool revertOnFail)
external
payable
returns (bool[] memory successes, bytes[] memory results)
{
successes = new bool[](calls.length);
results = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(calls[i]);
require(success || !revertOnFail, "call failed");
successes[i] = success;
results[i] = result;
}
}
The important part is this line:
address(this).delegatecall(calls[i]);
Because it uses delegatecall, every batched call keeps the same msg.sender and msg.value.
This is where it is commonly misused.
For example, imagine the same contract also has:
function deposit() external payable {
require(msg.value == 1 ether, "need 1 ETH");
credits[msg.sender] += 100;
}
Used normally, this looks fine:
- send 1 ETH
- call deposit()
- get 100 credits
But once deposit() can be reached through batching, one 1 ETH transaction can batch deposit() multiple times.
Because every batched call still sees the same msg.value, the contract may count the same 1 ETH again and again. So one payment can execute deposit() multiple times.
That is the danger. Batching itself is not automatically broken, but if developers batch functions that rely on msg.value or assume each call is independent, it can easily lead to vulnerabilities.
Using dork to find vulnerable Solidity contracts
One way to find vulnerable Solidity contracts is by using a dorking mindset.
For example, before Solidity 0.8, integer overflow and underflow on uint was still possible because arithmetic did not automatically revert.
So if we want to search for this vulnerability, we can search for an old Solidity version like:
pragma solidity 0.7
And to make it more accurate, we can look for contracts that do not use SafeMath. We can make it more specific by adding arithmetic patterns like:
+= or -=
So one way to search for this type of bug is to combine multiple clues in a dork.
That gives us a search idea like this:
site:etherscan*io "pragma solidity 0.7" "-="
or more specifically:
site:etherscan*io "pragma solidity 0.7" "accounts[_from].balance -= _value;"
But sometimes Google does not give accurate results for what we really want.
Because Google mostly searches exact text.
This is where EVM Chronicle has a dedicated feature for this kind of search.
It does not just search the exact words we type. It can search for the pattern we actually want.
So if you search:
pragma solidity 0.7
accounts[_from].balance -= _value;
it can return contract address results with similar logic, like:
balance[address] -= wad;
or other similar arithmetic patterns in old Solidity version 0.7.x.
With EVM Chronicle, you do not have to stress out selecting the exact words to match the data you want.
You can just place a line of code, and EVM Chronicle can extract the logic from there and search on its own dedicated database.
If you want a clearer picture, watch the video to see exactly how I do it in practice.






