low level vulnerablity

Use .call instead of .transfer to send ether .transfer will relay 2300 gas and .call will relay all the gas. If the receive/fallback function from the recipient proxy contract has complex logic, using .transfer will fail, causing integration issues.
Unbounded loop
1
2
3
4
5
6
7
8
9
10
11
function claimGovFees() public {
address[] memory assets = bondNFT.getAssets();

for (uint i=0; i < assets.length; i++) {
uint balanceBefore = IERC20(assets[i]).balanceOf(address(this));
IGovNFT(govNFT).claim(assets[i]);
uint balanceAfter = IERC20(assets[i]).balanceOf(address(this));
IERC20(assets[i]).approve(address(bondNFT), type(uint256).max);
bondNFT.distribute(assets[i], balanceAfter - balanceBefore);
}
}
Use time units directly
1
uint constant private DAY = 24 * 60 * 60;
Chainlink price feed is not sufficiently validated and can return stale price
1
2
3
(uint80 roundId, int256 assetChainlinkPriceInt, , uint256 updatedAt, uint80 answeredInRound) = IPrice(_chainlinkFeed).latestRoundData();
require(answeredInRound >= roundId, "price is stale");
require(updatedAt > 0, "round is incomplete");
  • Use the safe variant and ERC721.mint
  • Add an event for critical parameter changes
  • Declare interfaces on separate files
  • Constants should be upper case
  • Replace constant private with private constant

ERC1155Enumerable Implement

When minting and burning tokens,the ERC1155Enumerable implementation does not correctly update the following states:

  • uint256[] private _allTokens;
  • mapping(uint256 => uint256) private _allTokensIndex;
  • mapping(address => uint256) internal _currentIndex;

In particular:

  • the _allTokens array length (and therefore the totalSupply()) always increases (never decreases)
  • the _allTokensIndex[id] always increases
  • the _curentIndex[from] always increases
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

contract HelperERC1155 is ERC1155Enumerable, ERC1155Holder {
constructor() ERC1155("Test") {
}
function mint(uint256 id, uint256 amount) external {
_mint(msg.sender, id, amount, bytes(""));
}
function burn(uint256 id, uint256 amount) external {
_burn(msg.sender, id, amount);
}
function currentIndex(address owner) external view returns (uint256) {
return _currentIndex[owner];
}
function allTokensIndex(uint256 id) external view returns (uint256) {
return _allTokensIndex[id];
}
function allTokens(uint256 idx) external view returns (uint256) {
return _allTokens[idx];
}
function idTotalSupply(uint256 id) external view returns (uint256) {
return _idTotalSupply[id];
}
}

Solidity gas saving tips(to be continued)

Using delete instead of setting struct 0 saves gas
1
2
-   pool.long0ProtocolFees = 0;
+ delete pool.long0ProtocolFees;
Saving on gas costs should be prioritized
1
2
3
4
5
6
7
8
9
10
+    require(block.timestamp <= deadline, "KYCRegistry: signature expired");

require(
!kycState[kycRequirementGroup][user],
"KYCRegistry: user already verified"
);
- require(block.timestamp <= deadline, "KYCRegistry: signature expired");
bytes32 structHash = keccak256(
abi.encode(_APPROVAL_TYPEHASH, kycRequirementGroup, user, deadline)
);
The way to save on gas costs with a for loop
1
2
3
4
5
6
7
8
-    for (uint256 i = 0; i < exCallData.length; ++i) {

+ for (uint256 i = 0; i < exCallData.length;) {
+ unchecked {
+ ++i;
+ }
}
}
No necessary global variable read
1
2
3
4
5
-    doTransferOut(admin, reduceAmount);
+ doTransferOut(payable(msg.sender), reduceAmount);
- emit ReservesReduced(admin, reduceAmount, totalReservesNew);
+ emit ReservesReduced(msg.sender, reduceAmount, totalReservesNew);

Cache mapping instead of reading multiple times
1
2
3
4
5
6
-    if (fTokenToUnderlyingPrice[fToken] != 0) {
- return fTokenToUnderlyingPrice[fToken];
+ uint256 fToken = fTokenToUnderlyingPrice[fToken];
+ if(fToken != 0) {
+ return fToken;
}
Immutable save more gas
1
2
-  address public owner;
+ address public immutable owner;
mapping(address⇒bool) using bool for storage incurs overhead
1
2
-  mapping(address => bool) public allowedAsset;
+ mapping(address => uint256) public allowedAsset;
Save gas with the use of the import statement Solidity code is also cleaner in another way that might not be noticeable: the struct Point. We were importing it previously with global import but not using it. The Point struct polluted the source code with an unnecessary object we were not using because we did not need it. This was breaking the rule of modularity and modular programming: only import what you need Specific imports with curly braces allow us to apply this rule better.
Recommendation:
import {contract1 , contract2} from "filename.sol";
Sort Solidity operations using short-circuit mode //f(x) is a low gas cost operation //g(y) is a high gas cost operation //Sort operations with different gas costs as follows f(x) || g(y) f(x) && g(y)
Change for loop behavior by removing add (+1) and ++x is more gas efficient
1
2
3
4
5
6
7
8
function buy(uint256 _amount) external payable {
...
- for (uint48 x = sale_.currentId + 1; x <= newId; x++) {
+ for (uint48 x = sale_.currentId; x < newId; ++x) {
nft.mint(msg.sender, x);
}
...
}
  • Multiple access to mapping/array should use local variable cache
  • Duplicated require should be modifier or function
  • Use custom error rather than revert()/require()
  • Use calldata instead of memory for read-only variable
  • require()/revert() string longer than 32 bytes cost extra gas
  • Using Openzeppelin Ownable2Step.sol is gas efficient
  • Using UniswapV3 mulDiv function is gas-optimized
  • Use nested if and, avoid multiple check combinations
  • Avoid using state variable in emit (130 gas)
  • Instead of cache a whole object ,try cache single Attributes
  • Using int32 for time
  • Don’t use _msgSender() if not supporting EIP-2771
  • Using > 0 costs more gas than != 0 when used on a uint in a require() statement(version>0.8.13)
  • Using bools for storage incurs overhead
  • .length should not be looked up in every loop of a for-loop
  • Using calldata instead of memory for read-only arguments in external functions saves gas
  • Splitting require() statements that use && saves gas
  • Use a more recent version of solidity
  • require()/revert() strings longer than 32 bytes cost extra gas
  • += costs more gas than = + for state variables
  • Using storage instead of memory for structs/arrays saves gas

Audit Daily Approved Tokens To A Pool Can Be Forced To Take

[H] Audit Daily Approved Tokens To A Pool Can Be Forced To Take

Code snippet

full code link

https://github.com

1
2
3
4
5
6
7
8
9
10
function take(
address borrowerAddress_,
uint256 collateral_,
address callee_,
bytes calldata data_
) external override nonReentrant {
...

_transferQuoteTokenFrom(callee_, result.quoteTokenAmount);
}

Impact

Anyone who invokes the toke function and passes in someone else who approved this pool could take the tokens.

Proof of concept

I use foundry to make the whole test.

  • 1.Firstly,we need to create our own ERC20 token and implement the quote contract.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function setUp() public virtual {
console.log("When transferring tokens...");
alice = address(2);
bob = address(3);

myERC20 = new MyERC20();
quote = new Quote(address(myERC20));

//owner mint some token
myERC20.mint(OWNERAMOUNT);

//send some token to our dear testing user.
myERC20.transfer(alice,SOMEAMOUNT);
myERC20.transfer(bob,SOMEAMOUNT);
}
  • 2.Use bob’s address approved the pool some token.Check the balance of bob after we make the call.
1
2
3
4
5
vm.prank(bob);
myERC20.approve(address(quote), SOMEAMOUNT);

//check bob balance step1.
assertEq(myERC20.balanceOf(bob), SOMEAMOUNT);
  • 3.Use Alice’s address to call the take function which is public within the quote contract.
1
2
3
//alice transfer the approve tokens.
vm.prank(alice);
quote.take(address(bob), SOMEAMOUNT);
  • 4.Finally, we check the balance of bob, if bob’s token has been token by anyone who makes the call.
1
2
//check bob balance step2.
assertEq(myERC20.balanceOf(bob), 0);

Alternatively, consider checking that callee has approved spending quote tokens to msg.sender.

1
2
3
4
5
6
7
8
9
10
function take(
address borrowerAddress_,
uint256 collateral_,
address callee_,
bytes calldata data_
) external override nonReentrant {
...

_transferQuoteTokenFrom(msg.sender, result.quoteTokenAmount);
}

Audit Daily Selfdestruct May Cause The Funds To Be Lost

[H] Audit Daily Selfdestruct May Cause The Funds To Be Lost

Code

https://github.com

1
2
3
4
5
6
7
8
9
function buy() external payable {
_end();
}

function _end(Sale memory _sale) internal {
emit End(_sale);
ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20);
selfdestruct(_sale.saleReceiver);
}

Impact

After the contract is destroyed,the subsequent execution of the contract’s function buy() is going on.That causes the msg.value token to be lost in the contract forever.

Proof of concept

Note:When there is no code at the address, the transaction will succeed, and the msg.value will be stored in the contract.

Let’s say Alice and bob are invoking the contact simultaneously. The transactions are sent to the mempool. Alice is finished executes her transaction when bob is still waiting for his result. And then the contract is destroyed.
Finally, bob finished his transaction and sent his token to this contract. This way bob’s token is lost and locked forever in this empty contract.

Instead of using self-destruct, we could modify the state to represent the contract has completed the process.We could modify the code like this:

1
2
3
4
5
6
function _end(Sale memory _sale) internal {
ISaleFactory(factory).feeReceiver().transfer(address(this).balance / 20);
- selfdestruct(_sale.saleReceiver);
+ sale.finalId = sale.currentId
+ sale.saleReceiver.transfer(address(this).balance);
}