Unlike mobile and web development, in which software can be upgraded easily to fix any security loopholes, the same is arduous if not impossible in the field of blockchain, smart contracts in particular.
The cost of failure in blockchain can be extremely high as a lot of currency is at stake.
Keeping this in mind, the smart contract developer needs a different engineering mindset and seriousness when writing Solidity code, considering the security threats and exploitations, compared to other technology software developers.
Ethereum, the widely used and most commercially successful blockchain, is a neutral, open-source, publicly visible, immutable public ledger. Without a backup strategy, a vulnerability will remain open indefinitely. It is possible to upgrade the smart contract to fix the bug or exploit, but it is only a workaround solution.
This post discusses the ownership or access control vulnerability, how can the attacker can exploit it, and last, the potential solutions.
Let’s begin the journey.
The Exploit
The ownership exploit can be explained with an example. Consider a smart contract for setting the price of a commodity such as real estate.
// SPDX-License-Identifier: MIT pragma solidity 0.8.9; contract RealEstatePrice { uint256 public apartmentprice; constructor(uint256 _price) { apartmentprice = _price; // default } function updateApartmentPrice(uint256 _price) external { apartmentprice = _price; } }
The contract defined above is a simple real estate price, the constructor sets the default price for the apartment.
The updateApartmentPrice()
updates the apartment price with the new one.
The contract appears innocent, however, if you observe closely, the function updateApartmentPrice()
is an external function and can be called by anyone (attacker) apart from the deployer or the owner to update the apartment pricing.
This is a simple and classic example of an ownership attack where an attacker can call a function to update the value and easily exploit it.
Solution
There are two possible solutions to the above-mentioned problem. One uses the custom modifier and the other uses OpenZeppelin’s Owner contract.
Solution 1
We can add a custom modifier that checks if you are the owner of the contract and only then allows you to update the price in the function.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract RealEstatePrice { uint256 public apartmentprice; address owner; constructor(uint256 _price) { apartmentprice = _price; // default owner = msg.sender; // init owner } modifier CheckForOwner(){ require(msg.sender == owner, "Access Denied!"); _; } function updateApartmentPrice(uint256 _price) external CheckForOwner { apartmentprice = _price; } }
In the above solution, we introduced an owner
variable, initialized with msg.sender
during initialization in the constructor.
The modifier CheckForOwner()
makes the check if the address is the same as the owner’s address.
The modifier can be called as part of the function updateApartmentPrice()
to ensure that the caller is the owner before making any update in the pricing.
This solution prevents the attacker from modifying the critical variable such as apartmentprice
. There is another cleaner solution possible by using OpneZeppelin’s owner contract.
Solution 2:
As OpenZeppelins contracts are well tested, proven, efficient, and widely adopted we can reuse the owner smart contract, preventing us from rewriting the modifier like above.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts/access/Ownable.sol"; contract RealEstatePrice is Ownable { uint256 public apartmentprice; constructor(uint256 _price) { apartmentprice = _price; // default } function updateApartmentPrice(uint256 _price) external onlyOwner { apartmentprice = _price; } }
In the above solution, we import the Ownable
contract from OpenZeppelin. The contract already has the modifier onlyOwner
which checks for the ownership of the msg.sender
.
This solution is widely used in multiple production-grade contracts as it provides a clean interface.
Conclusion
To conclude, in this post we saw how the functions that are public or external can be easily called by the attacker to modify the variables in the contract and the possible solutions.
In the upcoming posts, we will explore more exploits in the contracts and how they can be prevented. Happy hacking!