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

How to use google spread sheet to generate merkle tree for ERC20 token airdrop

1.what’s merkle tree

Refer to the definetion of merkle tree on wikipedia:https://en.wikipedia.org/wiki/Merkle_tree
In cryptography and computer science, a hash tree or Merkle tree is a tree in which every “leaf” node is labelled with the cryptographic hash of a data block, and every node that is not a leaf (called a branch, inner node, or inode) is labelled with the cryptographic hash of the labels of its child nodes. A hash tree allows efficient and secure verification of the contents of a large data structure. A hash tree is a generalization of a hash list and a hash chain.
As we know, an airdrop may contain numerous eligible addresses. Storing all these addresses would incur significant gas fees. Instead, we can simply store the Merkle root, which is a 32-byte value.
If you’re not a Solidity developer, grasping the basics of Merkle trees can be challenging. That’s why we’ve developed a tool called oneclicktoken. It simplifies the process of generating Merkle tree roots and, furthermore, enables the creation of ERC20 tokens with just a click. You won’t need any prior knowledge of web3, Solidity, or the ERC20 standard
At this early stage of our project, we’d like to highlight some of the features and supported chains

1.1 supported chains:

  • base
  • sepolia(test)
  • More Coming…

1.2 features:

  • generate merkle root from google spread sheet
  • generate ERC20 token
    • Premint
    • Mintable
    • Pauseable
    • VerifyContract(if you need)

2.How to generate merkle tree root from an online google spread

eg:https://docs.google.com/spreadsheets/d/1jVX3j-TlLtI3VJ-27DuOuZuCM3x6_zf9W5tI6k67TvQ/edit#gid=0
Currently, we only support the following format:

1
2
--------A----------|-----------B-----------|
claimer(address) amount(uint256)

2.1 get params

  • id
  • sheet name

To extract the unique ID from a Google Sheets URL, you can locate the string of characters following the /d/ and preceding the /edit in the URL. For example, in the given URL, the unique ID is 1jVX3j-TlLtI3VJ-27DuOuZuCM3x6_zf9W5tI6k67TvQ.

Next, to identify the specific sheet within the spreadsheet, navigate to the bottom left corner of the Google Sheets interface, where a list of sheets is displayed. From there, you can determine the name of the sheet you need to access. In the provided URL, the sheet name is sheet1.

Once you have obtained the unique ID and sheet name, you can proceed to our tool website, oneclicktoken . There, input the required parameters and click the Calculate button. This will generate the Merkle tree root and provide a download URL for the Merkle tree JSON file.

3.How to generate ERC20 with airdrop using a single click?

  • token name
  • token symbol
  • premint amount(1 == 1 ether token)
  • contract owner(deployer is the default owner)
  • merkle root(if you need an airdrop)
    Once you have obtained above params, navigate to our webiste oneclicktoken input the required parameters and click the deploy button , then you can deploy your custom ERC20 token to target chains. If you need to test your token you can deploy it to sepolia testnet first it’s free.

Our project is in the very early stages, so you might encounter some issues while using it. If you need assistance, please don’t hesitate to reach out to me through the following channels. Additionally, if you require a custom airdrop website or need assistance building a custom Solidity contract, feel free to contact me.

4.origin

https://blog.oneclicktoken.xyz/24-05-14.html

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

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?

OlympusRBS2

What is Olympus?

  • Defi
  • Leverages
  • Swap(RBS)
  • Loans

OHM && gOHM

OHM aims to fill the gap between fiat stablecoins and volatile crypto assets, providing relative stability and scalability in a fully autonomous way with no off-chain dependencies
OHM:0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5

gOHM:0x0ab87046fBb341D058F17CBC4c1133F25a20a52f

Treasury

he Treasury represents all assets owned and controlled by the protocol

Protocol Owned Liquidity

Dex

Uniswap v3 pool OHM/wETH

RBS

Provide OHM/DAI LP

Range Bound Stability

Cooler Loans

Cooler Loans is a decentralized lending facility that allows OHM (Olympus) token holders to borrow DAI by using their gOHM (governance OHM) tokens as collateral.

Cross-Chain Bridge

The OHM token is available cross-chain! Olympus utilizes LayerZero
Ethereum: 0x45e563c39cddba8699a90078f42353a57509543a
Arbitrum: 0x20B3834091f038Ce04D8686FAC99CA44A0FB285c

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