Audit review for 2024-04-panoptic from code4rena

https://github.com/code-423n4/2024-04-panoptic-findings

High Risk

1.SettleLongPremium is incorrectly implemented: premium should be deducted instead of added

summary

1
2
3
4
5
            // current available assets belonging to PLPs (updated after settlement) excluding any premium paid
int256 updatedAssets = int256(uint256(s_poolAssets)) - swappedAmount;

// add premium to be paid/collected on position close
> int256 tokenToPay = -realizedPremium;

In the code, the int type is used for numbers, and the sign of the subtrahend is used to control whether the final result is an increase or a decrease.

root cause

As a result, an error in the sign of the input parameters leads to the final operation being contrary to the expectation.

learned

When we use the int type, we need to pay special attention to the sign of the values

2.

summary

1
2
3
4
5
6
7
unchecked {
assets = Math.mulDivRoundingUp(
shares * DECIMALS,
totalAssets(),
totalSupply * (DECIMALS - COMMISSION_FEE)
);
}

unchecked block can lead to silent overflow

root cause

Due to user can mint huge amounts of shares for free the shares can be much greater than expected.

learned

Double check the unchecked block make sure it can’t overflow.

Medium Risk

1.PanopticFactory uses spot price when deploying new pools, resulting in liquidity manipulation when minting

summary

1
@>	(uint160 currentSqrtPriceX96, , , , , , ) = v3Pool.slot0();

When deployNewPool is called it uses the spot price of the pool, which can be manipulated through a flashloan and thus could return a highly inaccurate result.

root cause

uses spot price

learned

spot price can be manipulated through a flashloan, try to use TWAP price instead.

2._validatePositionList() does not check for duplicate tokenIds, allowing attackers to bypass solvency checks

summary

The user can pass in an array of tokenIds, but there is no check for duplicates.

root cause

no check for duplicates

learned

Think about if pass in array need to be checked for duplicates.

3.Removed liquidity can overflow when calling SemiFungiblePositionManager.mintTokenizedPosition function

summary

1
2
3
4
5
6
if (!isBurn) {
// we can't remove more liquidity than we add in the first place, so this can't overflow
unchecked {
removedLiquidity += chunkLiquidity;
}
}

options can be repeatly which can lead to removedLiquidity sum up mutiple times.

root cause

unchecked block overflow

learned

need to double check the unchecked block make sure it can not be overflow

4.Wrong leg chunkKey calculation in haircutPremia function

summary

when loop tokenId , uses always the index 0 when calculating the leg chunkKey instead of using the actual leg index

root cause

logic error

learned

pay more attention to the logic.

5.Panoptic pool can be non-profitable by specific Uniswap governance

summary

The developers only considered the current range of Uniswap fees, but these fees may change through governance, affecting the existing logic.

root cause

fee may changes in the future.

learned

It is necessary to consider the impact of governance on the current fees.

Audit review for 2024-03-zivoe from sherlcok

https://github.com/sherlock-audit/2024-03-zivoe-judging/issues

1.DAO unable to withdraw their funds due to Convex admin action

summary

The administrator can cause a DoS (Denial of Service) in the protocol by passing malicious parameters

root cause

according to docs : admin’s action is RESTRICTED

learned

When a role’s behavior is RESTRICTED, it is necessary to examine the consequences of any suspicious actions

2.Inadequate Allowance Handling in convertAndForward Function of OCT_DAO & OCT_YDL

summary

stricted allowance assertion check lead to transaction failed

root cause

protocol suffers from inadequate handling of token allowances for the 1inch router,however they are not reset afterward.

learned

take care of the allowance assertion check

3.cannot forward extra rewards from both OCY_Convex to OCT_YDL

summary

1
2
-               if (rewardAmount > 0) { IERC20(rewardContract).safeTransfer(OCT_YDL, rewardAmount); }
+ if (rewardAmount > 0) { IBaseRewardPool_OCY_Convex_C(rewardContract).rewardToken().safeTransfer(OCT_YDL, rewardAmount); }

root cause

use safeTransfer in a none erc20 contract

learned

4.ZivoeYDL::earningsTrancheuse() always assumes that daysBetweenDistributions have passed, which might not be the case

summary

The protocol relies on keepers to call distributeYield. However, there is no guarantee that the keeper will make the call immediately.

root cause

The calculation of the APY depends on block.timestamp.

learned

When the calculation of APY depends on the timestamp, ensure it is called immediately

5.ZivoeYDL::distributeYield yield distribution is flash-loan manipulatable

summary

distributeYield is depends on totalSupply, however totalSupply can be manipulable through a flashloan.A 1-transaction inflated staked amount allows to inflate stakers distribution at the loss of vesters distribution

root cause

distributeYield amount is calculated with totalSupply

6.distributeYield() calls earningsTrancheuse() with outdated emaSTT & emaJTT while calculating senior & junior tranche yield distributions

summary

The earningsTrancheuse function uses emaSTT and emaJTT to calculate earnings, and then updates the latest emaSTT and emaJTT. It is recommended to update the latest values first and then use them to calculate earnings.

root cause

the value is not lastest need to be updated.

7.User cannot withdraw stakingToken due to incorrect calculation of _totalSupply

summary

When the user revokes a stake, the total amount is subtracted instead of the currently withdrawable amount, leading to an overflow DoS.

root cause

logic error

Audit H/M report Summary from sherlock olympus

1.getBunniTokenPrice wrongly returns the total price of all tokens

The function getBunniTokenPrice() is supposed to return the price of 1 Bunni token (1 share) like all other feeds, but it doesn't. It returns the total price of all minted tokens/shares for a specific pool (total value of position's reserves) which is wrong.

2.Possible incorrect price for tokens in Balancer stable pool due to amplification parameter update

3.Incorrect StablePool BPT price calculation

`getProtocolOwnedLiquidityOhm` is in scope , but any user can invoke deposit to add assets, deposit is not in scope.need to check those out of scope functions.

4.getReservesByCategory() when useSubmodules =true and submoduleReservesSelector=bytes4(0) will revert

when add not check parameters lead to some category can't get Reserves

5.Price calculation can be manipulated by intentionally reverting some of price feeds

when use uniswap and balancer to calculate price, both price feed can revert due to re-enter lead to user can manipulate price.

Audit H/M report Summary from c4 rl

0.learned from this contest

  • use selfdestruct can send eth to any contract,even contract not implement receive{}

1.Bidder can use donations to get VerbsToken from auction that already ended.

protocol use address(this).balance to check if bid amount is bigger than current eth value in contract , attacker can send eth to protocol to bypass above check.
need to take care of address(this).balance check.

2.Incorrect amounts of ETH are transferred to the DAO treasury in ERC20TokenEmitter::buyToken(), causing a value leak in every transaction

need to track eth

3.VerbsToken.tokenURI() is vulnerable to JSON injection attacks

4.encodedData argument of hashStruct is not calculated perfectly for EIP712 singed messages in CultureIndex.sol

hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))

5.ERC20TokenEmitter::buyToken function mints more tokens to users than it should do

6.Anyone can pause AuctionHouse in _createAuction #195

when mint revert the protocol gonna be paused,attacker can use specific amount of gas to send transaction which can lead to transaction OOG.

Audit H/M report Summary from NextGen C4

1.Attacker can reenter to mint all the collection supply

when mint ERC721 token,protocol invoke minter’s onERC721Received function can lead to re-entrancy issue.

2.Attacker can drain all ETH from AuctionDemo when block.timestamp == auctionEndTime

3.Adversary can block claimAuction() due to push-strategy to transfer assets to multiple bidders

claimAuction() implements a push-strategy instead of a pull-strategy for returning the bidders funds.
protocol invoke user’s address to send eth,user can implement revert on receive{} function to brick the process.

4.Multiple mints can brick any form of salesOption 3 mintings

1
2
lastMintDate[col] = collectionPhases[col].allowlistStartTime
+ (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1));

due to mint at different period , first mint period extend time result in lastMintDate bigger than second period’s start time.

5.MinterContract::payArtist can result in double the intended payout

inconsistent between set percent and calculate acturally pay out.

Audit H/M report Summary from Notional sherlock

1.Weighted pool spot price calculation is incorrect

Notional calculate the spot price of Weighted pool using the balances of token. Which need to be upscaled according to the balancer but Notional doesn’t.

Not farmilar with balancer wighted pool.

2.Single-sided instead of proportional exit is performed during emergency exit

Single-sided instead of proportional exit is performed during emergency exit, which could lead to a loss of assets during emergency exit and vault restoration.

Comment should use proportionally withdraw but used single token out , pay more attention on comments.

3.Native ETH not received when removing liquidity from Curve V2 pools

Not farmilar with Curve V2 pool removing LP.

4.Different spot prices used during the comparison

5.Incorrect invariant used for Balancer’s composable pools

Not farmilar with balancer need to check balancer contract next time.

6.Fewer than expected LP tokens if the pool is imbalanced during vault restoration

The vault restoration function intends to perform a proportional deposit

If the pool is imibalanced not 1:1?

Audit H/M report Summary from Ethena code4rena

1.FULL_RESTRICTED Stakers can bypass restriction through approvals

The openzeppelin ERC4626 contract allows approved address to withdraw and redeem on behalf of another address so far there is an approval。
Encountering an unfamiliar EIP requires a thorough understanding of its code before proceeding with the subsequent audit.

2.Soft Restricted Staker Role can withdraw stUSDe for USDe

The code does not satisfy that condition, when a holder has the SOFT_RESTRICTED_STAKER_ROLE, they can exchange their stUSDe for USDe using StakedUSDeV2。
When faced with strictly limiting conditions, it is essential to consider whether there are any possible ways to bypass them through various means.

3.users still forced to follow previously set cooldownDuration even when cooldown is off (set to zero) before unstaking

The main issue with this question is that when setting global parameters, the impact on previous users was not taken into consideration

4.Malicious users can front-run to cause a denial of service (DoS) for StakedUSDe due to MinShares checks

This issue arises from the fact that the first user of the vault can have a certain impact on subsequent users. Therefore, it is important to be mindful of the effects of the initial configuration when handling shares.

Audit H/M report Summary from WildCat code4rena

1.check contract exist using bytes32(0)

<address>.codehash != bytes32(0) check is insufficient to determine if an address has existing code
An account is considered empty when it has no code and zero nonce and zero balance.
if anyone transfers 1 wei to an address, .codehash will return keccak256("") instead of bytes32(0)

Fix for this is easier than suggested - just change from x.codehash != bytes32(0) to x.code.length != 0

2.Borrower has no way to update maxTotalSupply of market or close market

Developer define a function but no way to interact with it

3.When withdrawalBatchDuration is set to zero lenders can withdraw more then allocated to a batch

wirte a full test 

4.Any address can withdraw from markets contrary

function access control

5.Borrower can drain all funds of a sanctioned lender

missmatched parmeters in different functions.

Audit H/M report Summary from Allo sherlock

1.protocol use CREATE3 that’s not avaliable in the Zksync Era.

2.two modifer not cover block.timestamp == allocationEndTime.

1
2
3
4
5
6
7
8
9
10
11
12
13
function _checkOnlyActiveAllocation() internal view {
if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) {
revert ALLOCATION_NOT_ACTIVE();
}
}

/// @notice Checks if the allocation has ended and reverts if not.
/// @dev This will revert if the allocation has not ended.
function _checkOnlyAfterAllocation() internal view {
if (block.timestamp < allocationEndTime) {
revert ALLOCATION_NOT_ENDED();
}
}

When block.timestamp == allocationEndTime none of above modifers revert.

3.Manager vote every recipient to update his state once reacted the threshold

Since we have serval states protocol not check if user have already reacted the threshold.

4._distribute amount not correctly calculated.

5.missing access modifer

6.Can not create a pool by cloning strategies on zkSync network

Can not create pool by cloning strategies on zkSync network because of different behaviors from EVM instructions between zkSync and Ethereum

7.RFPSimpleStrategy milestones can be set multiple times

1
if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();

the value of upcomingMilestone not be updated after set milestones lead to manager can set milestones mutiple times.

8.fundPool does not work with fee-on-transfer token

protocol increase amount directly

origin

https://oneclicktoken.xyz/23-10-17.html

contact me for SC private review

Twitter:https://twitter.com/coffiasse
Github:https://github.com/coffiasd
TG:@coffiasd

Audit H/M report Summary from Ondo code4rena

1.Chain support chain cannot be removed or cleared in bridge contracts.

Contracts provide a way to add support for a chain; however, they do not provide a way to delete it.
When a contract offers an addition method, it’s important to consider whether it should also provide a deletion method

2.Contract use msg.sender as remote chain receiver when bridge token.

AA wallet like safe has different wallet address on different chains.
This vulnerability requires us to be familiar with some commonly used third-party tools in order to discover it

3.Two different transactions can result in the same txnHash value, thus breaking the approval process of transaction minting

1
txnHashToTransaction[txnHash] = Transaction(srcSender, amt); 

Consider whether the keys in similar mappings are always unique?

4.Admin can’t burn tokens from blocklisted addresses because of a check in _beforeTokenTransfer

The burn operation prematurely calls the beforeTransfer function, but beforeTransfer needs to check if the address is blocked, which hinders the successful execution of the burn

origin

https://oneclicktoken.xyz/23-10-13.html

contact me for SC private review

Twitter:https://twitter.com/coffiasse
Github:https://github.com/coffiasd
TG:@coffiasd

Audit H/M report Summary from xETH code4rena

1.Invalid Validation

Percentage of xETH in the Curve pool is between REBALANCE_UP_THRESHOLD and REBALANCE_DOWN_THRESHOLD. Rebalance operations are allowed When percentage of xETH is out of range.But the amount of xETH not checked which could lead to Percentage out of range after rebalance.

2.Invalid Validation

The value of the check does not match the expectation

3.ERC20

zero amount token transfer can cause DOS.

4.ERC20

check allowance == 0 should only use in initial or resetting to zero.

5.ERC20

when totalSupply() == 0

6.ERC20

bc of xETH.balanceOf(address(this)),staker can inflate the exchange rate by transferring tokens directly to the contract

7.MEV bot

uncorrect calculate slippage value.

8.Context

an array has add but no remove function.

9.ERC20

transfer token to target contract but can’t withdraw

origin

https://oneclicktoken.xyz/23-08-21.html

contact me

Twitter:https://twitter.com/coffiasse
Github:https://github.com/coffiasd
TG:@coffiasd

Audit H/M report Summary from Lybra code4rena

1.Access Control

The recipient of the flash loan will burn a certain amount of tokens as a fee. The recipient can be set to an arbitrary user who impelement receiver.onFlashLoan function.

In certain token-consuming operations, it’s necessary to consider whether only oneself is able to perform this action

2.Context

3.Common

Incorrectly implemented modifiers.

Always check modifiers revert?

4.Upgradable

Logic contact constructor should not update storage variable,cuz it will not be reflected in the proxy’s state.Instead we need use initializer function.

5.Precision Lost

user share can be burned cuz _EUSDAmount.mul(_totalShares).div(totalMintedEUSD) to zero.

6.Context

When using conditional statements, verify whether the values on the left and right sides of the equation meet the expected criteria.

7.Context

threshold value if different from the explanation in the document.

8.Dos

use IVault(pool).vaultType() vaultType can’t be a internel variable.

9.Math(M4)

when calculate token per second use wrong total amount.

10.Math(M6)

use a fixed 1e18 when calculating reward.

11.Context(M7)

not checking locking state when withdraw fund.

12.Context(M8)

calculate total debt not include fee.

How to use chainlink on your own evm contract

what is blockchain oracle?

As we know , blockchains are self-contained worlds.Smart contract can’t get information by invoking api like we do in web2.At the same time,centralized data providers are also not very reliable.For example,we need to know the price ETH/USDC when we swaping on defi like uniswap.Can we trust one single centralized data provider?The answer is obvious.So we need some decentralized data providers.Oracel act as an message bridge between blockchain and off-chain data provider.

why do SMART CONTRACTS need oracle?

To achieve deterministic execution, blockchains limit nodes to reaching consensus on simple binary (true/false) questions using only data stored on the blockchain itself.If blockchain get information from external data provider it would be impossible to achieve determinism.Different blockchain node maybe return different executes result.To solve those problems we create oracles.Oracle smart contract taking information from off-chain sources and then store the information on the blockchain.Since information stored on-chain is unalterable and publicly available, Ethereum nodes can safely use the oracle imported off-chain data to compute state changes without breaking consensus

Let’s dive into an example,assume that we need to get the BTC/USD price on our smart contract.
Chainlink provide data feed server on several networks.

  • Ethereum Mainnet
  • Sepolia Testnet
  • Gogerli Testnet

We gonna test the data feed server on goerli testnet by foundry tool.

First we need to import chainlink contract interface @chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol.

Secondly,let’s init the smart contract using a specify target smart contract address which we copy from chainlink official document website.

Lastly,we just need invoke latestRoundData function which return the current price about BTC/USD.

Here is the whole code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract DataConsumerV3 {
AggregatorV3Interface internal dataFeed;

constructor() {
dataFeed = AggregatorV3Interface(
0xA39434A63A52E749F02807ae27335515BA4b07F7
);
}

function getLatestData() public view returns (int) {
// prettier-ignore
(
/* uint80 roundID */,
int answer,
/*uint startedAt*/,
/*uint timeStamp*/,
/*uint80 answeredInRound*/
) = dataFeed.latestRoundData();
return answer;
}
}

we can use forge create command to deploy the contract.And then we invoke the getLatestData function via cast call command.Trust me you’ll get some return like this:

1
2
eth_goerli
0x000000000000000000000000000000000000000000000000000002a6386b1c96

If you are using Chainlink Data Feeds on L2 networks like Arbitrum, Optimism, and Metis, you must also check the latest answer from the L2 Sequencer Uptime Feed to ensure that the data is accurate in the event of an L2 sequencer outage

origin

https://oneclicktoken.xyz/23-07-28.html

contact me

Twitter:https://twitter.com/coffiasse
Github:https://github.com/coffiasd
TG:@coffiasd

All you need to know about openzeppelin access control

what is access control

Alt access control

Basiclly,access control means “who is allowed to do this thing”.It’s quite important for smart contract to specify a particular address who can totally control the whole system.Therefor it’s critical to fully understand how to use access control before using it in your project or just copy some example code from somewhere.
In openzeppelin there are mainly two ways to implementing access control.

  • Single onlyOwner role
  • Role-Based Access Control (RBAC)

Let’s dive into how to master this 2 ways.

master how to use it by practice

OnlyOwner

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
function normalThing() public {
// anyone can call this normalThing()
}

function specialThing() public onlyOwner {
// only the owner can call specialThing()!
}
}

let’s create two user for our test.

  • owner address
  • arbitrary address
1
2
address owner = address(0x100);
address arbitrary = address(0x101);

and the we use owner address to create the smart contract,it’s quite simple you don’t even need to pass any parameters.openzeppelin Ownable would set msg.sender as it’s owner as default.we use foundry for testing,if you’re not familiar with foundry,go and learn it first before proceeding further.

Firstly,we use owner address create our smart contract above in foundry test contract setUp function.

1
2
3
4
5
//let's set address owner to msg.sender.
vm.prank(owner);

//create the contract.
myContract = new MyContract();

And then we use the arbitrary address to invoke the specialThing function which is protected by onlyOwner modifier.As expect the invoke revert because of access control.Here goes the code:

1
2
3
4
//use arbitrary address invoke specialThing() function.
vm.prank(arbitrary);
vm.expectRevert("Ownable: caller is not the owner");
myContract.specialThing();

Finally,we use our owner who master the contract invoke the specialThing function.As you see the owne succeed.

1
2
3
//switch to owner.
vm.prank(owner);
myContract.specialThing();

If you want the whole example codes go for:https://gist.github.com/coffiasd/84552b5a33845fa567bfc3aa5204d460
If you have any questions, you can also reach me through the contact information below.

Role-Based Access Control

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken2 is ERC20, AccessControl {
// Create a new role identifier for the minter role
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

constructor(address minter) ERC20("MyToken", "TKN") {
_grantRole(DEFAULT_ADMIN_ROLE, minter);
// Grant the minter role to a specified account
_grantRole(MINTER_ROLE, minter);
}

function mint(address to, uint256 amount) public {
// Check that the calling account has the minter role
require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
_mint(to, amount);
}
}

We can create whatever role we want in Role-Based Access Control (RBAC) contract.Let’s say we want create a minter role who can mint some ERC20 token.We can use a special constant bytes32 as the name of the role like this:

1
2
// Create a new role identifier for the minter role
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

Firstly,we use owner address as our minter to create our access control contract.

1
myToken = new MyToken2(owner);

And then we use the arbitrary user to mint some ERC20 token:

1
2
3
4
//use arbitrary address invoke mint() function.
vm.prank(arbitrary);
vm.expectRevert("Caller is not a minter");
myToken.mint(address(this), 1);

the above invoke would be revert because of arbitrary don’t hold the minter role.

1
2
3
//switch to owner.
vm.prank(owner);
myToken.mint(address(this), 1);

If we switch msg.sender to owner the call would successed.

What’s more? we can use owner address to grant a minter role whoever we want.Let’s say we grant the minter role to the arbitrary address:

1
2
3
4
5
vm.prank(owner);
myToken.grantRole(keccak256("MINTER_ROLE"), arbitrary);

vm.prank(arbitrary);
myToken.mint(address(this), 1);

After invoke the grantRole function the arbitrary has the right to mint token.

At the same time,arbitrary address don’t has the right to grant role to another address:

1
2
3
vm.prank(arbitrary);
vm.expectRevert();
myToken.grantRole(keccak256("MINTER_ROLE"), address(0x1001));

As the owner we also have the right to revoke role we grant before:

1
2
3
4
5
6
vm.prank(owner);
myToken.revokeRole(keccak256("MINTER_ROLE"), arbitrary);

vm.prank(arbitrary);
vm.expectRevert("Caller is not a minter");
myToken.mint(address(this), 1);

After we invoke role of arbitrary,he can’t mint ERC20 token anymore.

If you want the whole example codes go for:https://gist.github.com/coffiasd/340241d63980dc9d423e4ece2f9b20db
If you have any questions, you can also reach me through the contact information below.

let’s connect

Twitter:https://twitter.com/coffiasse
Github:https://github.com/coffiasd
TG:@coffiasd

How to use solidity CREATE and CREATE2

Overview

There are 2 major ways to deploy smart contract CREATE and CREATE2. They look quite similar, but there are still some differences.CREATE2 and CREATE are one of solidity opcode which give us the ability to predict the address where a contract will be deployed before we deploy a smart contract.You might be curious about what opcode is.Opcodes are the fundamental building blocks of EVM bytecode, and each opcode represents a specific operation that the EVM can perform. For example, there are opcodes for arithmetic operations, logical operations, storage access, memory manipulation, conditional branching, and more.

What is CREATE

Smart contracts can be created both by other contracts and regular EOA.They both compute the new address the same way:

1
new_address = keccak256(sender, nonce)

So each created address is associated with a nonce value.Nonce increased on every transaction.It is related to the number of transactions we make and is unpredictable. That’s why we need CREATE2.

What is CREATE2 ?

The whole idea behind this opcode is to make the resulting address independent of future events. Regardless of what may happen on the blockchain, it will always be possible to deploy the contract at the precomputed address.There are four parameters in CREATE2 function:

  • 0xFF
  • the address of sender
  • A salt (random value from sender)
  • bytecode (the code of the new contract)
1
new_address = keccak256(0xFF, sender, salt, bytecode)

Why we need CREATE2 ?

Imagine a scenario where you need to deploy a contract to multiple networks, and precisely at that moment, you need to store the addresses of the yet-to-be-deployed contracts as storage parameters within the currently deployed contract. In such cases, you would require knowing the future addresses of the contracts to be deployed in advance.

How to use it

Use hash function to calculate address.

1
2
3
4
5
6
7
8
9
uint256 _salt = 1;
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
keccak256(type(TokenTemplate).creationCode)
)
);

Use deploy to get the deployed address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function deploy(
bytes memory bytecode,
uint _salt
) public payable returns (address) {
address addr;

assembly {
addr := create2(
callvalue(), // wei sent with current call
// Actual code starts after skipping the first 32 bytes
add(bytecode, 0x20),
mload(bytecode), // Load the size of code contained in the first 32 bytes
_salt // Salt from function arguments
)

if iszero(extcodesize(addr)) {
revert(0, 0)
}
}

return addr;
}

you can find the whole code on github:
https://gist.github.com/coffiasd/c2ff1a2d718d286b9064873be2fe079a

Follow my twitter:https://twitter.com/coffiasse
TG:@coffiasd

The principle and proof of Solidity Merkle trees

what is merkle trees

Definition

Merkle trees, named after Ralph Merkle, are a fundamental data structure used in cryptography and computer science. They are commonly employed in blockchain systems to ensure data integrity and enable efficient verification.

The principle behind Merkle trees is based on the concept of hash functions. A hash function is a mathematical algorithm that takes an input (data) and produces a fixed-size output, known as a hash value or digest. The key properties of a hash function are collision resistance and the avalanche effect.

how to generate merkle tree data via solidity

we can calculate hash value through solidity keccak256 function:

1
2
3
4
5
6
7
8
keccak256(abi.encodePacked(toHashValue)

e.g.:
before hash
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

after hash
0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb

After performing the hash operation on the values of the leaf nodes, the adjacent nodes are then hashed together until only a single root node remains.

Suppose there are two adjacent nodes, A and B. The order of the hash operation, whether it is hash(A+B) or hash(B+A), is determined by the sizes of A and B. In the corresponding Merkle code in OpenZeppelin, we can find the following code snippet:

1
2
3
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
}

In summary, the smaller value is typically placed in front to determine the order for computing the hash values. This may cause some confusion when performing the calculations manually.

In practical projects, it is common to store only the final result, the root hash value, in the contract. Storing a large number of addresses in the contract would consume a significant amount of gas fees. By using a Merkle tree calculation, the amount of data that needs to be stored is greatly reduced.

Let’s demonstrate how to calculate and store the root hash value using a setup example from Foundry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

bytes32 public root;
bytes32[] public leafs;
bytes32[] public l2;

function setUp() public {
address[] memory addrss = new address[](4);
addrss[0] = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
addrss[1] = 0x2d886570A0dA04885bfD6eb48eD8b8ff01A0eb7e;
addrss[2] = 0xed857ac80A9cc7ca07a1C213e79683A1883df07B;
addrss[3] = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990;

//Calculating the hash values of leaf nodes from a list of addresses.
leafs.push(keccak256(abi.encodePacked(addrss[0])));
leafs.push(keccak256(abi.encodePacked(addrss[1])));
leafs.push(keccak256(abi.encodePacked(addrss[2])));
leafs.push(keccak256(abi.encodePacked(addrss[3])));

//Calculating the hash values of the second level.
l2.push(keccak256(abi.encodePacked(leafs[0], leafs[1])));
l2.push(keccak256(abi.encodePacked(leafs[2], leafs[3])));

//Calculating root hash value.
root = keccak256(abi.encodePacked(l2[1], l2[0]));
}

For demonstration purposes, we have provided only four addresses. In actual projects, the number of addresses can be significantly larger.

how to proof merkle data

Once we have stored the root hash value in the contract, how do we verify if a client-provided address is a valid address or belongs to a whitelist?

Firstly, we need to hash the address as the third parameter. Then, we pass the hash value of the address, along with the adjacent hash values, as the proof to the verification function.

The proof list corresponds to the red-marked area in the image below.

test proof function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function testVerify() public {
address proofAddress = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;

bytes32[] memory proof = new bytes32[](2);
proof[0] = leafs[1];
proof[1] = l2[1];

assert(
MerkleProof.verify(
proof,
root,
keccak256(abi.encodePacked(proofAddress))
)
);
}

Here is the complete Foundry testing code from github:https://gist.github.com/coffiasd/96022abd7f9a8a2189bda14bf9e755dc

Applications in Real-World Projects

  • airdrop
  • whitelist

Common Vulnerabilities in Smart Contract Audits

1
2
3
4
5
6
7
function parentHash(bytes32 a, bytes32 b) public pure returns (bytes32) {
if (a < b) {
return keccak256(abi.encode(a, b));
} else {
return keccak256(abi.encode(b, a));
}
}

The statement “abi.encode(address, uint)” will produce 64 bytes. Since “abi.encode(bytes32, bytes32)” also yields 64 bytes, hash collisions may potentially occur between leaf nodes and parent nodes.

Follow me on twitter :https://twitter.com/coffiasse
TG:@coffiasd

Merkle树的逻辑和证明

什么是Merkle树

定义

Merkle Tree,也叫默克尔树或哈希树,是区块链的底层加密技术,被比特币和以太坊区块链广泛采用。Merkle Tree是一种自下而上构建的加密树,每个叶子是对应数据的哈希,而每个非叶子为它的2个子节点的哈希。

如何生成Merkle树的数据

在solidity中我们通过keccak256算法计算hash值:

1
2
3
4
5
6
7
8
keccak256(abi.encodePacked(toHashValue)

e.g.:
hash前
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

hash后
0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb

在对叶子节点的值进行hash运算之后,再把相邻的节点再进行hash运算,直到只剩下一个根节点。
假设存在两个相邻的节点A和B,那么在进行hash运算的时候到地址是hash(A+B)呢?还是hash(B+A)呢?其实这是由A和B的大小决定的,在openzeppelin对应的merkle代码中我们可以找到这么一段代码:

1
2
3
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
}

总结来说就是把相对小的数值放到前面去这么来排序计算hash值。这个地方在自己动手实际运算的时候可能会有些许困惑。
在实际的项目中一般只需要把计算的最后结果的根hash值存储到合约中,如果大量的地址都需要存到合约中的话会消耗大量的gas费。经过merkle树计算之后,大大的减少了需要存储的数据。
通过一段foundry的setUp演示下如何计算和存储root hash值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

bytes32 public root;
bytes32[] public leafs;
bytes32[] public l2;

function setUp() public {
address[] memory addrss = new address[](4);
addrss[0] = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
addrss[1] = 0x2d886570A0dA04885bfD6eb48eD8b8ff01A0eb7e;
addrss[2] = 0xed857ac80A9cc7ca07a1C213e79683A1883df07B;
addrss[3] = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990;

//通过地址列表计算叶子节点的hash值
leafs.push(keccak256(abi.encodePacked(addrss[0])));
leafs.push(keccak256(abi.encodePacked(addrss[1])));
leafs.push(keccak256(abi.encodePacked(addrss[2])));
leafs.push(keccak256(abi.encodePacked(addrss[3])));

//计算第二层的hash值
l2.push(keccak256(abi.encodePacked(leafs[0], leafs[1])));
l2.push(keccak256(abi.encodePacked(leafs[2], leafs[3])));

//计算根的hash值
root = keccak256(abi.encodePacked(l2[1], l2[0]));
}

为了演示方便我们值写了4个地址,实际项目中可能地址数量非常大。

如何来验证Merkle树

在合约中存储到root hash值之后我们如何去验证由客户端发过来的地址是否是有效地址或者说在白名单中的地址呢?
首先我们需要将地址进行hash运算,作为第三个参数,然后将地址相邻的hash值作为proof传到验证函数中。
proof列表对应下面图片中的红色标记区域

测试的验证方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function testVerify() public {
address proofAddress = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;

bytes32[] memory proof = new bytes32[](2);
proof[0] = leafs[1];
proof[1] = l2[1];

assert(
MerkleProof.verify(
proof,
root,
keccak256(abi.encodePacked(proofAddress))
)
);
}

完整的foundry测试代码:https://gist.github.com/coffiasd/96022abd7f9a8a2189bda14bf9e755dc

在实际项目中的应用场景

  • 发放空投
  • NFT的白名单

在合约审计中的常见漏洞

1
2
3
4
5
6
7
function parentHash(bytes32 a, bytes32 b) public pure returns (bytes32) {
if (a < b) {
return keccak256(abi.encode(a, b));
} else {
return keccak256(abi.encode(b, a));
}
}

abi.encode(address, uint)将会输出64字节。由于abi.encode(bytes32, bytes32)也是64字节,因此在叶子节点和父节点之间可能会发生哈希碰撞。

Follow我的推进行交流学习:https://twitter.com/coffiasse
TG:@coffiasd

solidity可升级合约

可升级合约的概念和目的

可升级合约是一种智能合约设计模式,旨在允许对已部署的智能合约进行更新和修改,而无需中断现有的应用程序或迁移用户数据。其目的是解决智能合约不可变性的限制,允许在合约生命周期内进行必要的修复、功能增强或业务逻辑变更。

传统的智能合约一旦部署在区块链上,其代码和状态是永久不可更改的。这在某些情况下会限制应用程序的灵活性和可维护性,尤其当出现漏洞、安全问题或需要业务升级时。

可升级合约的概念通过设计一种合约结构,使得合约的逻辑和数据可以在合约生命周期内进行更新和升级。这通常涉及将合约的逻辑和数据分离,使用委托调用(DelegateCall)等技术手段实现。通过这种方式,合约可以更新其逻辑部分,而无需改变存储状态或中断已部署的合约的操作。

可升级合约的设计模式

我们通常使用的可升级合约主要由2部分组成:

  • 代理合约
  • 逻辑合约

其中代理合约主要用于存储数据比如我们指向的逻辑合约的地址就需要存储在代理合约中。逻辑合约不存储业务相关的任何数据,只提供逻辑执行代码。这个具体的调用过程归功于solidity中的DelegateCall方法。假设在合约A中执行DelegateCall,调用合约B,那么合约B的代码就会被执行,数据最终存储到合约A之中。所以合约B中的数据是否修改对我们整个项目其实没有影响。

Follow我的推进行交流学习:https://twitter.com/coffiasse
TG:@coffiasd

Assembly

什么是Assembly

在编写Solidity代码时,我们可以使用assembly{}关键字开始编写Yul代码,它是一种简化且扩展了的汇编语言。通过使用assembly,我们可以直接访问堆栈,并优化代码以提高内存效率,从而减少执行交易所需的燃气量。这最终降低了用户的交易成本。

然而,在可读性方面存在一些妥协。许多前端开发人员可以阅读Solidity智能合约并理解正在执行的功能以及如何将其应用到他们的web3查询中。相比之下,汇编可能会让人感到有些困惑,如果你不熟悉低级编程,可能很难理解其逻辑和流程。

操作码列表

操作码 描述
add(x, y) 将栈中的前两个值相加,并用结果替换它们
sub(x, y) 减法操作
mul(x, y) 乘法操作
div(x, y) 除法操作
mod(x, y) 取模操作
lt(x, y) 小于。如果 x 小于 y,则返回 1
gt(x, y) 大于
eq(x, y) 等于。如果 x 等于 y,则返回 1
not(x) 按位取反 (1010 > 0101)
and(x, y) 按位与 (1000 AND 1100 > 1000)
or(x, y) 按位或 (1000 AND 1100 > 1100)
xor(x, y) 按位异或 (1000 XOR 1100 > 0100)
byte(n, x) X 的第 N 个字节
keccak256(pos, n) 本地哈希算法
pop(x) 从栈中弹出 X
mload(pos) 加载存储在位置 pos 的内存数据
mstore(pos, value) 在位置 pos 的memory中存储值 value
sload(pos) 加载存储在位置 pos 的存储数据
sstore(pos, value) 在位置 pos 的storage中存储值 value
balance(address) 以 wei 为单位返回地址的以太币余额
call(gas, address, value, in, insize, out, outsize) 调用外部合约,也可用于发送资金,成功时返回 1
delegatecall(gas, address, value, in, insize, out, outsize) 与上述相同,但由用户而不是合约进行调用
revert(p, s) 撤销事务和任何状态更改
return(p, s) 结束执行并返回数据

这里需要注意的是mstoresstore的区别,主要存储的位置的不一致,一种是存储在memory中,另一种是存储在storage中。

使用案例

读写

下面我们通过一个例子展示下如何使用Assembly把数据存储到storage中,然后通过函数读取的方式读出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function testAssembly() public {
//定义一个存储的值方便测试.
uint256 x = 123;
bool res;

//将x存储到storage中。
assembly {
sstore(0, x)
}

//在storage中读取x的值,其中0是pos
assembly {
let readX := sload(0)
//将readX的值存储到memory,pos == 0x80的位置。
mstore(0x80, readX)
}

//将0x80中的值取出来.
assembly {
let v := mload(0x80)
if eq(v, x) {
res := true
}
}

//判断最终取出来的v == x?
assert(res);
}

上诉的例子在实际项目中不太可能出现,只是为了展示读写的过程。

基础运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function testAssembly() public {
//定义一个存储的值方便测试.
uint256 x = 123;
uint256 r = 345;

//将x存储到storage中。
assembly {
x := add(x, 300)
x := sub(x, 78)
}

//判断最终取出来的r == x?
assert(r == x);
}

Follow我的推进行交流学习:https://twitter.com/coffiasse
TG:@coffiasd

foundry的介绍

什么是foundry?

foundry是一个solidity智能合约开发工具。可以帮你管理依赖包,编译项目,运行测试脚本,还可以让你通过命令行工具或者script脚本和链上合约进行交互。和hardhat不同的地方是,hardhat我们还是主要用来开发大型的合约项目,但是foundry用来进行编写测试脚本我认为是非常方便的。主要的特点是可以直接使用solidity编写合约测试脚本,无需在JavaScript和solidity之间进行切换。虽然在hardhat也可以编写测试脚本,但是有时候需要切换语言或者对类型进行转换相对会比较麻烦。

安装和配置

Foundryup是foundry的安装器。我们可以直接通过终端命令行进行安装:

1
curl -L https://foundry.paradigm.xyz | bash

或者要是你喜欢也可以通过源码的方式进行编译安装

1
2
3
4
5
6
7
8
9
10
# clone the repository
git clone https://github.com/foundry-rs/foundry.git
cd foundry
# install Forge + Cast
cargo install --path ./cli --profile local --bins --force
# install Anvil
cargo install --path ./anvil --profile local --force
# install Chisel
cargo install --path ./chisel --profile local --force

初始化

我们可以通过forge命令行直接初始化新项目

1
forge init helle_foundry

初始化之后的大概是这么个目录结构:

1
2
3
4
5
6
7
8
$ cd hello_foundry
$ tree . -d -L 1
.
├── lib 依赖的包文件目录
├── script 自定义的脚本目录
├── src 合约源码目录
└── test 测试脚本目录

特性和使用

编写测试脚本

测试脚本的基本代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity 0.8.10;

import "forge-std/Test.sol";

contract ContractBTest is Test {
uint256 testNumber;

function setUp() public {
testNumber = 42;
}

function test_NumberIs42() public {
assertEq(testNumber, 42);
}

function testFail_Subtract43() public {
testNumber -= 43;
}
}

在上面的例子中每个test函数都是相互独立的,setUp函数类似于我们的构造函数,会首先进行调用setUp函数进行初始化。在我们实际的操作中,可以直接导入需要测试的合约,然后在setUp中对合约进行初始化。我们只需要一个变量就能调用目标合约中的函数进行测试。

运行测试脚本的命令:

1
forge test --match-contract "ContractBTest(合约名称)" -vvv

debug

我们可以通过-vvv-vvvv进行数据跟踪。在打印出来的日志中,我们通过颜色来区分不同的错误类型(如果你的终端支持颜色显示的话):

  • 绿色 : 正常的调用
  • 红色 : 回滚的调用
  • 蓝色 : 作弊码的调用
  • 青色 : 日志
  • 黄色 : 合约部署
1
2
3
4
5
6
7
8
9
[24661] OwnerUpOnlyTest::testIncrementAsOwner()
├─ [2262] OwnerUpOnly::count()
│ └─ ← 0
├─ [20398] OwnerUpOnly::increment()
│ └─ ← ()
├─ [262] OwnerUpOnly::count()
│ └─ ← 1
└─ ← ()

fuzz测试

1
2
3
4
5
6
7
8
9
10
11
contract SafeTest is Test {

function testFuzz_Withdraw(uint256 amount) public {
payable(address(safe)).transfer(amount);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
}

比如上面的提款测试,普通的test测试函数需要自定义一个固定的amount金额,但是这个指定的amount不能覆盖到所有的情景,另一个选择就是使用testFuzz.fuzz测试的函数名和普通测试对比只是多了fuzz,需要注意的是所有的测试函数需要用test或者testFuzz开头否则是不会被认为是一个测试函数的。

console.log日志打印

在solidity中打印日志需要提前对日志内容的类型进行定义。foundry支持下面的一些数据类型进行日志打印,可以帮助我们进行debug:

  • console.logInt(int i)
  • console.logUint(uint i)
  • console.logString(string memory s)
  • console.logBool(bool b)
  • console.logAddress(address a)
  • console.logBytes(bytes memory b)
  • console.logBytes1(bytes1 b)
  • console.logBytes2(bytes2 b)
  • console.logBytes32(bytes32 b)
    对于一些更加复杂的数据结构,比如我们的目标合约中返回了一个struct类型的数据。我们可以直接使用targetContract.StructName对数据进行定义。

assert

foundry中主要有3种类型的assert

  • assert(conditon) 判断一个bool值
  • assertEq(a,b) 判断a,b两个值是否相等
  • assertTrue(condition,”reason”)
    1
    2
    3
    function testGetTotalShares() public {
    assertEq(prizePool.getTotalShares(), 220);
    }

时间和区块进行改变

某些合约中可能有时间间隔,比如一个stake asset操作之后,可能需要一个月的时间间隔之后才能进行下一次操作,这个时候我们可以直接去修改evm中的时间

1
vm.warp(block.timstamp + 30 days);

或者我们也可以对区块进行增加

1
vm.roll(block.number + 1);

vm.expectRevert

在某些测试中,对于条件不满足的情况下,合约逻辑中可能会存在一些revert的情况,我们需要对这些revert进行捕捉的时候可以用到vm.expertRevert

  • expectRevert()
  • expectRevert(bytes4 message)
  • expectRevert(bytes calldata message)
1
2
3
4
5
function testLowLevelCallRevert() public {
vm.expectRevert(bytes("error message"));
(bool revertsAsExpected, ) = address(myContract).call(myCalldata);
assertTrue(revertsAsExpected, "expectRevert: call did not revert");
}

有些合约对一些多次出现的错误可能把它定义为一个error类型,这个时候我们可以通过CustomError.selector对错误进行捕捉。

1
vm.expectRevert(CustomError.selector);
1
2
3
4
vm.expectRevert(
abi.encodeWithSelector(CustomError.selector, 1, 2)
);

对于那些直接revert并没有定义错误的情况下:

1
vm.expectRevert(bytes(""));

Follow我的推进行交流学习:https://twitter.com/coffiasse
TG:@coffiasd