Smart contracts are often described as the foundation for a new kind of financial system. They allow code to directly control digital money, with the promise of systems that run automatically, don’t rely on middlemen, and are open for anyone to inspect. Over the past decade, this idea has grown from an experiment into a large ecosystem that now holds billions of dollars across exchanges, lending platforms, digital assets, and other financial tools.
Ethereum has played a major role in this growth. As the first widely used smart contract platform, it introduced the Ethereum Virtual Machine (EVM) and the Solidity programming language, giving developers a way to build applications that run on a blockchain. Today, Solidity is still the most widely used language for building these systems, and much of the crypto economy depends on it.
But a financial system is only as reliable as the tools used to build it. Unlike traditional software, decentralised smart contracts cannot be fixed or updated once they are deployed. If there is a mistake in the code, it can’t easily be reversed, and funds can be permanently lost or moved. Because of this, the design of the programming language becomes extremely important. If the language allows mistakes to happen easily, those mistakes can directly turn into real financial losses.
The Ethereum smart contract language Solidity is an example of a prototype language. In this case, a prototype language is similar to a minimum viable product (MVP). It technically works, but rather than fix the language itself, developers are required to know the correct procedures for working with it. It’s like walking across a bridge made of popsicle sticks and duct tape. Instead of building a structurally sound bridge from the start, developers are expected to learn where it bends, where it might break, and how to step carefully to avoid falling through.
Solidity does have its merits, as it helped lay the foundation for blockchain-based development and enabled early progress in the space. In the context of our bridge, rather than reinforcing and rebuilding it to make it reliable, the ecosystem has adapted to its weaknesses. Developers are trained to navigate its flaws, even as more weight is placed on it over time, rather than fixing the structure itself.
Systems that manage large amounts of money need to meet a much higher standard than experimental or everyday software. They should be built to prevent mistakes from happening in the first place, not just explain how to avoid them. This means the language and system should include strong safety protections, limit confusing or risky behaviour, and make it hard or impossible for the system to enter a broken or unsafe state. The way the system runs should be clear and predictable, with no hidden actions that could be triggered unexpectedly. It should also be easy to test and understand how the system will behave in different situations. When real money is at risk, security cannot rely on developers remembering every rule or avoiding past mistakes. It needs to be built into the system itself.
Solidity does not meet these standards. Its design puts much of the responsibility for safety on the developer instead of the system itself. Rather than preventing common mistakes, it allows them to happen and expects developers to know how to avoid them. This means that even experienced teams can introduce serious bugs, not because they are careless, but because the language makes it easy to get small details wrong in ways that have big consequences. In a system where code directly controls money and cannot easily be fixed after deployment, this creates unavoidable risk. The problem is not just how developers use Solidity, but how Solidity is designed.
What “Good” Looks Like
To understand why this matters, it helps to look at how software used in traditional financial systems is designed.
In established financial systems, software is built with the assumption that mistakes will happen, and the system must prevent those mistakes from causing harm.
Modern programming environments and languages are designed to reduce risk by default. They include strong type systems, built-in safety checks, and restrictions that prevent many common errors from ever reaching production.
- Errors are prevented early
Many classes of bugs are caught during development, before the system is ever deployed. - Unsafe behaviour is restricted
Risky operations are limited, clearly marked, or require extra steps to use. - Systems are testable and upgradable
If something goes wrong, software can be patched, rolled back, or shut down safely. - Safety is built-in, not optional
Developers don’t need to remember every edge case as the system helps enforce correct behaviour.
Even within the world of smart contracts, newer approaches have begun to move in this direction by emphasising stricter control over state changes, clearer execution flow, and stronger guarantees about how contracts behave.
These systems are designed to reduce the chance of failure at every level. In contrast, Solidity often leaves these responsibilities in the hands of the developer, increasing the likelihood that small mistakes can lead to serious consequences.
Language Level vs Developer Responsibility
A key difference between secure systems and risky ones is where responsibility for safety is placed.
In well-designed systems, safety is enforced by the language and tools themselves. In others, safety depends on developers correctly using those tools and avoiding known pitfalls.
Relying on developers to remember every edge case or follow every best practice introduces risk, especially in environments where mistakes can have serious financial consequences.
Solidity places much of the burden of safety on the developer. It exposes low-level operations that can be misused, does not enforce safe behaviour by default, and allows contracts with known risky patterns to compile and run without restriction.
As a result, many common vulnerabilities are not caused by unusual or complex errors, but by normal usage of the language in ways that are easy to get wrong.
In systems that manage real money, safety cannot depend on perfect developer behaviour. It must be enforced by design.
The vulnerabilities that follow illustrate this problem in practice. In each case, the failure is not just that a developer made a mistake, but that Solidity permits risky behaviour by default and leaves it to the developer to prevent financial harm.
Reentrancy
Reentrancy is one of the most well-known vulnerabilities in smart contracts, but its importance goes beyond a single bug. It exists because of how Solidity allows contracts to interact with each other. The language permits execution to leave a contract and return to it before its internal state has been fully updated, without enforcing a safe pattern for doing so. This allows contracts to behave correctly under normal conditions, but fail under adversarial ones.
In simple terms, reentrancy happens when a contract sends funds to an external address before updating its own records. If that external address is another contract, it can call back into the original function before the first call finishes. Because the original contract has not yet updated its state, it can be tricked into sending funds multiple times.
This is similar to making a second purchase from your bank account before the first one has been processed. Even if you only have $100, you might be able to make two $80 purchases if the system checks your balance before updating it.
In practice, this happens when a contract allows users to withdraw funds and sends those funds using a low-level call before updating its internal balance tracking. If the recipient is a malicious contract, it can use this moment to call back into the withdrawal function again before its balance is reduced, repeating the process and draining funds.
Code Example:
function withdraw(uint _amount) {
require(balances[msg.sender] >= _amount);
msg.sender.call.value(_amount)();
balances[msg.sender] -= _amount;
}
This is not a problem that might someday happen. It has already led to one of the most significant failures in blockchain history.
The DAO Hack exploited this exact pattern. The DAO was a decentralised investment fund that raised over $150 million in ETH. An attacker discovered that its withdrawal logic sent funds before updating balances, allowing a malicious contract to repeatedly re-enter the function and withdraw funds multiple times.
Within hours, approximately 3.6 million ETH (worth tens of millions of dollars at the time) was drained. The scale of the attack forced the Ethereum community to take the extraordinary step of performing a hard fork to reverse the damage, splitting the network into two chains.
From an institutional perspective, this kind of vulnerability represents an unbounded financial risk. A single mistake in how a contract handles external calls can allow funds to be withdrawn repeatedly, far beyond what should be possible under normal conditions. In traditional financial systems, this would be equivalent to a failure that allows unlimited withdrawals from an account, with no safeguards or automatic stop mechanisms. Such a system would fail basic requirements around risk management, predictability, and control. It would also raise serious concerns around liability and compliance, as there would be no reliable way to prevent, contain, or reverse the damage once it occurs.
The key issue is not that developers sometimes forget to follow best practices, but that the language allows this pattern in the first place. Solidity does not enforce the correct order of operations or prevent unsafe external calls during sensitive state changes. Instead, it relies on developers to remember patterns such as updating state before making external calls or using additional protections like reentrancy guards. In a system where real money is at stake, this is a fundamental design flaw. Safety depends on developers getting the details exactly right, rather than being enforced by the language itself.
Access Control
Access control vulnerabilities are some of the most dangerous issues in smart contracts because they decide who is allowed to control money. In Solidity, these problems don’t usually come from complicated code, but from how developers define who is “in charge.” The language doesn’t enforce strong or safe defaults, which makes it easy to accidentally give the wrong person access.
Think of a smart contract like a secure vault with buttons that only certain people are allowed to press. Access control issues happen when the vault doesn’t properly check who is pressing those buttons.
Two common ways this goes wrong:
tx.origin: This is like checking who originally started a chain of actions, instead of who is actually pressing the button right now. An attacker can trick you into starting something (like clicking a malicious link), and then their contract presses the button on your behalf. The vault sees you as the origin and allows it even though you didn’t intend it.
delegatecall: This is like letting an outsider step inside your vault and run their own instructions but with full access to everything. If that outsider is malicious or buggy, they can change your funds, permissions, or rules, because they’re acting as if they were you.
Another common issue happens during setup. Some contracts use a function to assign ownership after deployment, but forget to lock it so it can only be used once. This is like installing a vault and leaving the “set owner” button active. Anyone who finds it later can press it and take control.
These problems highlight a bigger issue: Solidity doesn’t force developers into safe patterns for handling ownership. It allows risky designs like reassigning ownership, trusting the wrong caller, or handing control to external code without built-in safeguards. As a result, everything depends on the developer getting every detail exactly right.
Code Example:
function initContract() public {
owner = msg.sender;
}
In 2017, an exploit affecting multi-signature wallets built by Parity Technologies allowed attackers to drain over 150,000 ETH. These wallets relied on a shared library contract to manage ownership through the ‘delegatcall’ feature which executes external code in the context of the calling contract.
The issue arose because the library contained an initialisation function that was intended to be called only once, but this restriction was not enforced. Because of how the contracts were structured, an attacker was able to call this function after deployment and replace the wallet’s owners with their own address.
Once ownership was reassigned, the attacker was able to transfer all funds out of the wallets. Despite the use of multi-signature protection, the entire security model failed because control over ownership was not properly restricted.
From an institutional perspective, this type of vulnerability represents a complete failure of authorisation control. A single mistake in how ownership is defined or enforced can allow an attacker to take full control of a system and its funds. In traditional finance, this would be equivalent to an unauthorised party gaining administrative access to accounts or infrastructure, with the ability to transfer assets without approval. Such a system would fail basic requirements around access control, accountability, and compliance, as there would be no reliable way to ensure that only authorised parties can act.
The core issue is not simply that developers make mistakes, but that Solidity allows these mistakes to directly determine who has control over funds. The language does not enforce safe ownership models, restrict dangerous patterns like reinitialisation, or provide strong guarantees around authorisation. Instead, it relies on developers to implement these protections correctly. In systems managing real financial value, this is a fundamental design flaw. Control over assets should not depend on perfect implementation. It should be enforced by the system itself.
Arithmetic Issues (Integer overflow/underflow)
Arithmetic vulnerabilities in Solidity reveal a deeper design issue. For years, the language allowed core financial calculations—such as balances, transfers, and total supply—to behave incorrectly without any warning or failure. In most modern systems, numerical errors are caught automatically. In Solidity, they were not.
In simple terms, an overflow or underflow happens when a number goes outside its allowed range. Instead of failing, the value “wraps around” to something unexpected.
For example, imagine a system where your balance is 0 and you subtract 1. Instead of rejecting the transaction, the system might interpret that as an extremely large number. It now believes you have more money than you actually do.
In a financial system, this is a critical failure of accounting integrity. The system can lose track of how much money exists and who owns it.
This issue appears in everyday contract logic, such as withdrawals:
Code Example:
function withdraw(uint _amount) {
require(balances[msg.sender] - _amount > 0);
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount;
}
If ‘_amount’ is larger than the user’s balance, the subtraction can underflow and turn into a very large number instead of failing. This causes the check to pass, allowing the attacker to withdraw more funds than they actually have.
This is not just a theoretical issue—it has already led to real-world failures.
In 2018, the BeautyChain (BEC) token was exploited using a batch transfer function that relied on multiplication to calculate total transfer amounts. Because Solidity did not check for overflow at the time, an attacker was able to input extremely large values that caused the calculation to wrap around to a very small number.
The contract used this incorrect value in its balance check, allowing the attacker to pass validation while still receiving the full transfer amounts. As a result, massive quantities of tokens were effectively created out of thin air, destroying the token’s economic integrity and causing its value to collapse.
From an institutional perspective, this represents a failure of basic financial correctness. Arithmetic errors of this kind mean that balances, totals, and supply can become inaccurate without detection. In traditional finance, this would be equivalent to a system where account balances can be corrupted or money can be created or destroyed due to calculation errors. Such a system would fail fundamental requirements around accounting integrity, auditability, and regulatory compliance.
Financial systems depend on numbers being correct. If arithmetic cannot be trusted, the system itself cannot be trusted.
Newer versions of Solidity introduced automatic overflow and underflow checks, causing transactions to fail instead of silently producing incorrect results.
At first glance, this appears to solve the problem. But in reality, it highlights a deeper issue.
For years, the language allowed critical financial logic to behave unpredictably by default, placing the burden of correctness entirely on developers. Even today, these protections are not absolute. Developers can explicitly disable them using unchecked blocks, and their effectiveness depends on compiler version and implementation choices.
Safety was introduced reactively and remains optional.
This is not just an arithmetic issue. It is another example of Solidity allowing unsafe behaviour unless developers actively prevent it.
In systems that manage real financial value, correctness cannot depend on developer awareness, version selection, or optional safeguards. It must be guaranteed by the system itself.
Unchecked Return Values for low level calls
Low-level calls in Solidity reveal a deeper design issue: the system can fail silently without stopping.
In simple terms, a contract can attempt to send money or perform an action, fail to do so, and still continue as if everything worked, unless the developer explicitly checks for failure.
A useful way to think about this is a bank system that tries to send money. The transfer fails, but the system still records the transaction as “completed.” The records say the payment happened, but in reality, nothing was sent.
This issue appears in common contract logic, such as withdrawals:
Code Example:
function withdraw(uint256 _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
etherLeft -= _amount;
msg.sender.send(_amount);
}
At a glance, this looks correct:
- Check the user has enough balance
- Subtract the amount
- Send the money
But there is a flaw.
If the ‘send()’ call fails, for example if the receiving contract rejects the transfer, the function does not stop. The contract has already updated its internal records.
So now:
- The system believes the money was sent
- But the user never received anything
This creates a mismatch between what the contract believes and what actually happened.
These issues are more subtle than obvious exploits like reentrancy, but they have appeared repeatedly in real systems. Contracts that fail to check whether these calls succeed can slowly drift out of sync, where internal balances no longer match real funds. Sometimes this locks funds permanently. Other times it causes inconsistencies that only surface much later.
What makes this especially dangerous is that nothing appears broken at first. The system continues running, quietly accumulating errors.
From an institutional perspective, this is a fundamental failure of accounting integrity.
It is equivalent to a financial system recording transactions that never actually occurred. A ledger shows money was paid out, but no payment was made. In traditional finance, this would immediately fail audits and reconciliation checks and would be considered unacceptable.
The deeper issue is not simply developer error. It is that Solidity allows critical operations to fail without enforcing correct handling. The language does not automatically stop execution or roll back changes when something goes wrong. Instead, it relies on developers to manually detect and handle every failure case.
This is not just a coding mistake, it is another example of Solidity allowing unsafe behaviour unless developers actively prevent it.
In systems that manage real financial value, correct accounting cannot depend on whether a developer remembered to check a return value. It must be guaranteed by the system itself.
Short Address Attack
The short address attack highlights a broader issue in the Ethereum Virtual Machine: the system can accept malformed or incorrectly encoded input and still execute transactions.
Instead of strictly enforcing correct data formatting, the system may interpret improperly structured inputs in unexpected ways. In a financial context, this means a transaction can execute with different parameters than what the user intended.
In simple terms, the system may process “broken” data instead of rejecting it.
Smart contracts expect inputs in a very specific format. For example, a function might expect:
- a recipient address
- an amount to send
If this data is encoded incorrectly, such as an address that is shorter than expected, the missing bytes can shift how the rest of the data is interpreted.
Instead of failing, the system may adjust the input and continue execution.
For example:
- The address is shorter than expected
- The system pads the missing data
- Part of the amount value gets misaligned
As a result, the contract may interpret the amount incorrectly.
In some cases, this can cause the transfer amount to increase dramatically (such as sending 256 times more tokens than intended) simply due to how the input was encoded.
What makes this dangerous is that the transaction can still succeed. Nothing appears broken at the system level, even though the outcome is incorrect.
While this specific attack has not been exploited in isolation so far, it reflects a broader class of real-world issues. From an institutional perspective, this represents a failure of input integrity.
Transactions may execute with incorrect parameters, leading to unintended transfers or incorrect amounts being sent. In traditional financial systems, transaction data must be strictly validated before execution. Inputs are required to be well-formed, complete, and unambiguous.
A system that accepts malformed data and attempts to interpret it would fail basic requirements around:
- transaction accuracy
- reconciliation
- user protection
The deeper issue is not simply that developers or interfaces can encode data incorrectly. It is that the system does not enforce strict validation of inputs at every level.
Instead, it allows ambiguous or malformed data to be processed, placing the burden on developers and external systems to ensure correctness.
This is not just an input validation issue, it is another example of the system allowing unsafe behaviour unless developers actively prevent it.
In environments where financial transactions must be precise and predictable, execution cannot depend on perfect input handling. It must be enforced by the system itself.
Why Mitigations Don’t Solve the Problem
These issues do not exist in isolation. Smart contracts are often reused, combined, and built on top of shared infrastructure. A single mistake can spread across multiple systems, amplifying its impact and turning a small error into a systemic failure.
The vulnerabilities discussed earlier—reentrancy, access control failures, arithmetic errors, and silent call failures—are not isolated bugs. They are symptoms of a system that permits unsafe behaviour unless it is actively prevented.
Supporters of Solidity often argue that many of its early problems have been addressed over time. Improvements such as built-in overflow checks, widely used libraries, security audits, and defensive coding patterns are frequently presented as solutions.
These measures do help reduce risk. But they do not solve the underlying problem.
They do not change how the system behaves at its core. Instead, they act as layers added on top of a foundation that still allows unsafe behaviour by default.
In many cases, safe usage is not enforced, it is optional. Developers must deliberately choose the correct patterns, use the right tools, and apply them correctly. If they miss something, the system does not stop them.
As more safeguards are introduced, the system becomes more complex. Developers must understand not only the language itself, but also a growing set of rules, exceptions, and best practices. This increases the likelihood of mistakes, especially in systems where even small errors can have large financial consequences.
Security audits are often treated as a final line of defence, but they are not guarantees of correctness. They rely on human review, are limited in scope, and can miss subtle or unexpected issues, especially in complex, interconnected systems.
Taken together, these mitigations do not eliminate risk. They redistribute it.
Instead of being enforced by the system itself, safety is pushed onto developers, auditors, and external processes.
This is the core issue.
In systems responsible for managing real financial value, safety cannot depend on perfect implementation, layered defences, or human review. It must be built into the foundation of the system itself, not added on afterward through mitigation.
Conclusion
The risks outlined are not isolated issues. They are the result of a system that places responsibility for safety on developers rather than enforcing it by design.
Individual vulnerabilities can be patched or mitigated over time, but the underlying model remains unchanged. In any other financial context, a system that depends on perfect developer behaviour to remain secure would be considered unacceptable.
Yet within the smart contract ecosystem, this model has become the foundation for systems managing billions of dollars in value.
The question is no longer how to better secure these systems through additional tools, audits, or defensive patterns. It is why such a fragile foundation is being relied upon at all.
If blockchain-based financial infrastructure is to meet institutional standards, it must move beyond this approach. Safety cannot be layered on through mitigation, it must be built into the system itself.
Until then, these systems will continue to operate on a foundation that would not be accepted in any other serious financial system.

