
The Uniswap Standard, From Zero To Mastery
IntroductionThe Uniswap protocol was launched on November 2, 2018 to test the effectiveness of Automated Market Makers (AMMs). Since then, hundreds of Decentralized Exchanges (DEXs) have built upon the original founding concepts that made early DEXs so great. The theories that the Uniswap AMM makes use of are found all over the Decentralized Finance (DeFi) ecosystem. Understanding how tokens are accurately and safely swapped between users is of utmost importance whether you are designing a ne...

Exposing Merkle Trees and Cryptographic Proofs
IntroductionIn 1989, Ralph Merkle invented and patented the Merkle Tree, a data structure built for securing data with digital signatures. The structure was created for validating individual parts of a data set without needing the entire set of data to compare to; using less memory and much less computation time spent on larger sets of data. Satoshi Nakamoto was the first to bring this technology to the mainstream in Cryptocurrency as the base structure for ensuring consistency of the Bitcoin...
Web designer, blockchain dev, and security researcher. Contributing to/securing Immunefi, KaijuKingz, and BAO Finance.



The Uniswap Standard, From Zero To Mastery
IntroductionThe Uniswap protocol was launched on November 2, 2018 to test the effectiveness of Automated Market Makers (AMMs). Since then, hundreds of Decentralized Exchanges (DEXs) have built upon the original founding concepts that made early DEXs so great. The theories that the Uniswap AMM makes use of are found all over the Decentralized Finance (DeFi) ecosystem. Understanding how tokens are accurately and safely swapped between users is of utmost importance whether you are designing a ne...

Exposing Merkle Trees and Cryptographic Proofs
IntroductionIn 1989, Ralph Merkle invented and patented the Merkle Tree, a data structure built for securing data with digital signatures. The structure was created for validating individual parts of a data set without needing the entire set of data to compare to; using less memory and much less computation time spent on larger sets of data. Satoshi Nakamoto was the first to bring this technology to the mainstream in Cryptocurrency as the base structure for ensuring consistency of the Bitcoin...
Web designer, blockchain dev, and security researcher. Contributing to/securing Immunefi, KaijuKingz, and BAO Finance.
Share Dialog
Share Dialog

Subscribe to haruxe

Subscribe to haruxe
<100 subscribers
<100 subscribers
Thanks to growing demand on the Ethereum network, smart contract developers are continuing to research and implement new methods of reducing transaction costs. In the last year alone, enormous progress has been made to eliminate unnecessary opcode executions as well as optimizing storage in contracts.
While pushing hard for the lowest possible execution costs is breeding creativity and innovation, it is putting some protocols at risk due to unsafe implementations and unforeseen problems.
This post will get you up-to-date with the newest developments and recent zero-day vulnerabilities while also giving a security researcher’s perspective on each.
Earlier this month, user 0xriptide submitted a vulnerability via Immunefi regarding the Arbitrum bridge smart contract, featuring a bug involving an uninitialized proxy contract due to a vulnerable implementation of postUpgradeInit. You can see his original post here.
In the below code snippets, the public initialize() function that is standard in the TransparentUpgradeableProxy uses the initializer modifier that checks the first two storage slots to determine whether the function has already been called. That is all well and good, until we get to the “optimized” postUpgradeInit() function.
function initialize(IBridge _bridge, ISequencerInbox _sequencerInbox)
external
initializer
onlyDelegated
{
bridge = _bridge;
sequencerInbox = _sequencerInbox;
allowListEnabled = false;
__Pausable_init();
}
“
postUpgradeInit()(as seen below) … wipes slots 0, 1 & 2 and sets the bridge andallowListEnabledslots to new values — but leavessequencerInboxand the two booleans set by theintializermodifier empty!” (0xriptide)
function postUpgradeInit(IBridge _bridge)
external
onlyDelegated
onlyProxyOwner
{
uint8 slotsToWipe = 3;
for (uint8 i = 0; i < slotsToWipe; i++) {
assembly {
sstore(i, 0)
}
}
allowListEnabled = false;
bridge = _bridge;
}
sstore() and assembly in general are great tools to reduce costs, as the unnecessary checks that the EVM does to ensure execution goes according to plan are not used. As 0xriptide states in his post, the bridge contract was in danger also in part because Arbitrum removed this moot check (seen below) that ensured that the bridge address variable was set in order to presumably save on gas during initialization.
if (address(bridge) != address(0)) revert AlreadyInit();
This next vulnerability was submitted by a number of auditors during OpenSea’s Seaport audit contest on Code4rena. It involves the classic overflowing as well as typecasting; explicitly converting one type to another.
In every case, you must ensure that user input is handled accordingly to avoid truncating, that is in this case - losing data due to mismanaged types. If you aren’t familiar with these terms, follow along with the next exploit.
This vulnerability is located in OrderValidator.sol, which as the name implies, “contains functionality related to validating orders and updating their status”. More specifically this is in the _validateOrderAndUpdateStatus() function. It is a very complex function, so I did my best to keep the snippet brief.
struct AdvancedOrder {
OrderParameters parameters;
uint120 numerator;
uint120 denominator;
bytes signature;
bytes extraData;
}
function _validateOrderAndUpdateStatus(
AdvancedOrder memory advancedOrder,
CriteriaResolver[] memory criteriaResolvers,
bool revertOnInvalid,
bytes32[] memory priorOrderHashes
)
internal
...
{
...
OrderParameters memory orderParameters = advancedOrder.parameters;
...
bytes32 orderHash = _assertConsiderationLengthAndGetNoncedOrderHash(orderParameters);
...
uint256 filledNumerator = orderStatus.numerator;
uint256 filledDenominator = orderStatus.denominator;
if (filledDenominator != 0) {
...
unchecked {
_orderStatus[orderHash].isValidated = true;
_orderStatus[orderHash].isCancelled = false;
_orderStatus[orderHash].numerator = uint120(filledNumerator +
numerator);
_orderStatus[orderHash].denominator = uint120(denominator);
}
}
else {
_orderStatus[orderHash].isValidated = true;
_orderStatus[orderHash].isCancelled = false;
_orderStatus[orderHash].numerator = uint120(numerator);
_orderStatus[orderHash].denominator = uint120(denominator);
}
...
}
The issue lies in the fact that the user controlled parameter AdvancedOrder can be given a numerator and/or denominator value that is close to overflow like 2**118. In turn, when someone wants to fill the order, it does so by cross multiplying and
“[b]ecause … the
uint120truncation in OrderValidator.sol#L228-L248, thenumeratoranddenominatorare truncated to0and0respectively.”(SpearbitDAO)
Ultimately, this small opening allowed for a potential attacker to drain any approved tokens for the same consideration amount.
At first glance, these kinds of issues are truly difficult to understand due to the fact that these protocols are quite complex. It is important to recognize where the most common mistakes are made in order to discover vulnerabilities the developers may have missed. If you want to learn more about this issue, here is the original submission by SpearbitDAO with an included PoC.
// Un-optimized
revert("Unauthorized");
// Yul equivalent
let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(add(free_mem_ptr, 4), 32)
mstore(add(free_mem_ptr, 36), 12)
mstore(add(free_mem_ptr, 68), "Unauthorized")
revert(free_mem_ptr, 100)
// Peak gas savings
revert Unauthorized();
// Yul equivalent
let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x82b4290000000000000000000000000000000000000000000000000000000000)
revert(free_mem_ptr, 4)
Using custom errors is cheaper than a string error on both deployment and execution. Below the example of each is the same thing just in Yul - you can see that the revert(“Unauthorized”) uses 3 additional of our precious mstore()s.
Events are a great way to keep track of things that happen on-chain. They are quite efficient in their own right, but one thing you can do to make your events even more optimized that many people do not know is that you can use the indexed keyword when logging events with values of type uint, bool, and/or address to save on some gas costs.
This next example comes courtesy of the Art Gobblers project led by the team at Paradigm.
// Un-optimized
event GobblerClaimed(address user, uint256 gobblerId);
...
currentNonLegendaryId++;
gobblerId = currentNonLegendaryId;
emit GobblerClaimed(msg.sender, gobblerId);
// Peak gas savings
event GobblerClaimed(address indexed user, uint256 indexed gobblerId);
...
emit GobblerClaimed(msg.sender, gobblerId = ++currentNonLegendaryId);
One funky thing you may have also noticed in this example is the second parameter of the bottom event emission. Lets take a closer look:
emit GobblerClaimed(msg.sender, gobblerId = ++currentNonLegendaryId)
It is doing 3 things here:
updating gobblerId’s value
incrementing currentNonLegendaryId
using the updated gobblerId as the input

// Un-optimized
for (uint256 i = 0; i < 10; i++) {
...
}
// Peak gas savings
for (uint256 i; i < 10;) {
...
unchecked {
++i;
}
}
In every case, using unchecked{ ++i; } is cheaper due to a few things:
It doesn’t use a default value of 0 for i, saves 3 gas per declaration
Using the iterator ++i ( ++i uses 5 gas less than i++ )
Using the unchecked{} block - as it is impossible for the iterator to overflow or underflow. A combination of this and ++i saves at least 25 gas, however can be up to 80 gas under certain conditions
// Un-optomized
string internal constant _NAME = "Berry";
// Peak gas savings
bytes32 internal constant _NAME = "Berry";
Storing string constants are more expensive than bytes constants because they are dynamically sized and have certain limitations in the context of the EVM.
If you can fit a string literal into a bytes32 do it - using fixed sized variables are always cheaper. You will find most large protocols practically never use strings because of this exact reason.
When creating functions, it is always best to limit the accessibility to only where the function is actually called from. internal and private are both cheaper than public and external when called from inside the contract in some cases.
People have noted that public is more expensive than external when called, but from my testing in versions solidity >0.8.0 that is not the case ( perhaps in newer versions? ).
When a function without payable is called, one of the first operations is to ensure msg.value == 0 and will revert the call if any `ether` is sent with the transaction.
Using the payable modifier negates that and the contract will accept the transaction regardless of msg.value. This is notably useful for the constructor function.
Each instance of the payable modifier saves 24 gas over the alternative.
The effort to reduce transaction costs with new techniques can often be like playing with fire. Some people are being burned because they dropped one require() statement to save 5 gwei to deploy.
Regardless, security ALWAYS comes first. If you are 99.9% positive that your withdraw() function is not susceptible to a re-entrancy attack, that’s enough reason to keep your ReentrancyGuard modifier on your function. Users safety is much more important than saving a couple gwei.
Stay responsible, keep hunting, and keep learning. Learning small things at a time will yield large results.
You’ll find that crit soon Anon, cheers.
Thanks to growing demand on the Ethereum network, smart contract developers are continuing to research and implement new methods of reducing transaction costs. In the last year alone, enormous progress has been made to eliminate unnecessary opcode executions as well as optimizing storage in contracts.
While pushing hard for the lowest possible execution costs is breeding creativity and innovation, it is putting some protocols at risk due to unsafe implementations and unforeseen problems.
This post will get you up-to-date with the newest developments and recent zero-day vulnerabilities while also giving a security researcher’s perspective on each.
Earlier this month, user 0xriptide submitted a vulnerability via Immunefi regarding the Arbitrum bridge smart contract, featuring a bug involving an uninitialized proxy contract due to a vulnerable implementation of postUpgradeInit. You can see his original post here.
In the below code snippets, the public initialize() function that is standard in the TransparentUpgradeableProxy uses the initializer modifier that checks the first two storage slots to determine whether the function has already been called. That is all well and good, until we get to the “optimized” postUpgradeInit() function.
function initialize(IBridge _bridge, ISequencerInbox _sequencerInbox)
external
initializer
onlyDelegated
{
bridge = _bridge;
sequencerInbox = _sequencerInbox;
allowListEnabled = false;
__Pausable_init();
}
“
postUpgradeInit()(as seen below) … wipes slots 0, 1 & 2 and sets the bridge andallowListEnabledslots to new values — but leavessequencerInboxand the two booleans set by theintializermodifier empty!” (0xriptide)
function postUpgradeInit(IBridge _bridge)
external
onlyDelegated
onlyProxyOwner
{
uint8 slotsToWipe = 3;
for (uint8 i = 0; i < slotsToWipe; i++) {
assembly {
sstore(i, 0)
}
}
allowListEnabled = false;
bridge = _bridge;
}
sstore() and assembly in general are great tools to reduce costs, as the unnecessary checks that the EVM does to ensure execution goes according to plan are not used. As 0xriptide states in his post, the bridge contract was in danger also in part because Arbitrum removed this moot check (seen below) that ensured that the bridge address variable was set in order to presumably save on gas during initialization.
if (address(bridge) != address(0)) revert AlreadyInit();
This next vulnerability was submitted by a number of auditors during OpenSea’s Seaport audit contest on Code4rena. It involves the classic overflowing as well as typecasting; explicitly converting one type to another.
In every case, you must ensure that user input is handled accordingly to avoid truncating, that is in this case - losing data due to mismanaged types. If you aren’t familiar with these terms, follow along with the next exploit.
This vulnerability is located in OrderValidator.sol, which as the name implies, “contains functionality related to validating orders and updating their status”. More specifically this is in the _validateOrderAndUpdateStatus() function. It is a very complex function, so I did my best to keep the snippet brief.
struct AdvancedOrder {
OrderParameters parameters;
uint120 numerator;
uint120 denominator;
bytes signature;
bytes extraData;
}
function _validateOrderAndUpdateStatus(
AdvancedOrder memory advancedOrder,
CriteriaResolver[] memory criteriaResolvers,
bool revertOnInvalid,
bytes32[] memory priorOrderHashes
)
internal
...
{
...
OrderParameters memory orderParameters = advancedOrder.parameters;
...
bytes32 orderHash = _assertConsiderationLengthAndGetNoncedOrderHash(orderParameters);
...
uint256 filledNumerator = orderStatus.numerator;
uint256 filledDenominator = orderStatus.denominator;
if (filledDenominator != 0) {
...
unchecked {
_orderStatus[orderHash].isValidated = true;
_orderStatus[orderHash].isCancelled = false;
_orderStatus[orderHash].numerator = uint120(filledNumerator +
numerator);
_orderStatus[orderHash].denominator = uint120(denominator);
}
}
else {
_orderStatus[orderHash].isValidated = true;
_orderStatus[orderHash].isCancelled = false;
_orderStatus[orderHash].numerator = uint120(numerator);
_orderStatus[orderHash].denominator = uint120(denominator);
}
...
}
The issue lies in the fact that the user controlled parameter AdvancedOrder can be given a numerator and/or denominator value that is close to overflow like 2**118. In turn, when someone wants to fill the order, it does so by cross multiplying and
“[b]ecause … the
uint120truncation in OrderValidator.sol#L228-L248, thenumeratoranddenominatorare truncated to0and0respectively.”(SpearbitDAO)
Ultimately, this small opening allowed for a potential attacker to drain any approved tokens for the same consideration amount.
At first glance, these kinds of issues are truly difficult to understand due to the fact that these protocols are quite complex. It is important to recognize where the most common mistakes are made in order to discover vulnerabilities the developers may have missed. If you want to learn more about this issue, here is the original submission by SpearbitDAO with an included PoC.
// Un-optimized
revert("Unauthorized");
// Yul equivalent
let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(add(free_mem_ptr, 4), 32)
mstore(add(free_mem_ptr, 36), 12)
mstore(add(free_mem_ptr, 68), "Unauthorized")
revert(free_mem_ptr, 100)
// Peak gas savings
revert Unauthorized();
// Yul equivalent
let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x82b4290000000000000000000000000000000000000000000000000000000000)
revert(free_mem_ptr, 4)
Using custom errors is cheaper than a string error on both deployment and execution. Below the example of each is the same thing just in Yul - you can see that the revert(“Unauthorized”) uses 3 additional of our precious mstore()s.
Events are a great way to keep track of things that happen on-chain. They are quite efficient in their own right, but one thing you can do to make your events even more optimized that many people do not know is that you can use the indexed keyword when logging events with values of type uint, bool, and/or address to save on some gas costs.
This next example comes courtesy of the Art Gobblers project led by the team at Paradigm.
// Un-optimized
event GobblerClaimed(address user, uint256 gobblerId);
...
currentNonLegendaryId++;
gobblerId = currentNonLegendaryId;
emit GobblerClaimed(msg.sender, gobblerId);
// Peak gas savings
event GobblerClaimed(address indexed user, uint256 indexed gobblerId);
...
emit GobblerClaimed(msg.sender, gobblerId = ++currentNonLegendaryId);
One funky thing you may have also noticed in this example is the second parameter of the bottom event emission. Lets take a closer look:
emit GobblerClaimed(msg.sender, gobblerId = ++currentNonLegendaryId)
It is doing 3 things here:
updating gobblerId’s value
incrementing currentNonLegendaryId
using the updated gobblerId as the input

// Un-optimized
for (uint256 i = 0; i < 10; i++) {
...
}
// Peak gas savings
for (uint256 i; i < 10;) {
...
unchecked {
++i;
}
}
In every case, using unchecked{ ++i; } is cheaper due to a few things:
It doesn’t use a default value of 0 for i, saves 3 gas per declaration
Using the iterator ++i ( ++i uses 5 gas less than i++ )
Using the unchecked{} block - as it is impossible for the iterator to overflow or underflow. A combination of this and ++i saves at least 25 gas, however can be up to 80 gas under certain conditions
// Un-optomized
string internal constant _NAME = "Berry";
// Peak gas savings
bytes32 internal constant _NAME = "Berry";
Storing string constants are more expensive than bytes constants because they are dynamically sized and have certain limitations in the context of the EVM.
If you can fit a string literal into a bytes32 do it - using fixed sized variables are always cheaper. You will find most large protocols practically never use strings because of this exact reason.
When creating functions, it is always best to limit the accessibility to only where the function is actually called from. internal and private are both cheaper than public and external when called from inside the contract in some cases.
People have noted that public is more expensive than external when called, but from my testing in versions solidity >0.8.0 that is not the case ( perhaps in newer versions? ).
When a function without payable is called, one of the first operations is to ensure msg.value == 0 and will revert the call if any `ether` is sent with the transaction.
Using the payable modifier negates that and the contract will accept the transaction regardless of msg.value. This is notably useful for the constructor function.
Each instance of the payable modifier saves 24 gas over the alternative.
The effort to reduce transaction costs with new techniques can often be like playing with fire. Some people are being burned because they dropped one require() statement to save 5 gwei to deploy.
Regardless, security ALWAYS comes first. If you are 99.9% positive that your withdraw() function is not susceptible to a re-entrancy attack, that’s enough reason to keep your ReentrancyGuard modifier on your function. Users safety is much more important than saving a couple gwei.
Stay responsible, keep hunting, and keep learning. Learning small things at a time will yield large results.
You’ll find that crit soon Anon, cheers.
No activity yet