Boot Finance contest - Reigada's results

Custom DEX AMM for Defi Projects

General Information

Platform: Code4rena

Start Date: 04/11/2021

Pot Size: $50,000 USDC

Total HM: 20

Participants: 28

Period: 7 days

Judge: 0xean

Total Solo HM: 11

Id: 51

League: ETH

Boot Finance

Findings Distribution

Researcher Performance

Rank: 5/28

Findings: 6

Award: $3,273.77

🌟 Selected for report: 10

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: Reigada

Also found by: WatchPug

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

1308.4274 USDC - $1,308.43

External Links

Handle

Reigada

Vulnerability details

Impact

As we can see in the contracts AirdropDistribution and InvestorDistribution, they both have the following approve() call: mainToken.approve(address(vestLock), 2**256-1); https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L499 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L80

This is necessary because both contracts transfer tokens to the vesting contract by calling its vest() function: https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L544 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L569 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L134 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L158

The code of the vest() function in the Vesting contract performs a transfer from msg.sender to Vesting contract address -> vestingToken.transferFrom(msg.sender, address(this), _amount); https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L95

Same is done in the BasicSale contract: https://github.com/code-423n4/2021-11-bootfinance/blob/main/tge/contracts/PublicSale.sol#L225

The problem is that this contract is missing the approve() call. For that reason, the contract is totally useless as the function _withdrawShare() will always revert with the following message: revert reason: ERC20: transfer amount exceeds allowance. This means that all the mainToken sent to the contract would be stuck there forever. No way to retrieve them.

How this issue was not detected in the testing phase? Very simple. The mock used by the team has an empty vest() function that performs no transfer call. https://github.com/code-423n4/2021-11-bootfinance/blob/main/tge/contracts/helper/MockVesting.sol#L10

Proof of Concept

See below Brownie's custom output: Calling -> publicsale.withdrawShare(1, 1, {'from': user2}) Transaction sent: 0x9976e4f48bd14f9be8e3e0f4d80fdb8f660afab96a7cbd64fa252510154e7fde Gas price: 0.0 gwei Gas limit: 6721975 Nonce: 5 BasicSale.withdrawShare confirmed (ERC20: transfer amount exceeds allowance) Block: 13577532 Gas used: 323334 (4.81%)

Call trace for '0x9976e4f48bd14f9be8e3e0f4d80fdb8f660afab96a7cbd64fa252510154e7fde': Initial call cost [21344 gas] BasicSale.withdrawShare 0:3724 [16114 / -193010 gas] β”œβ”€β”€ BasicSale._withdrawShare 111:1109 [8643 / 63957 gas] β”‚ β”œβ”€β”€ BasicSale._updateEmission 116:405 [53294 / 55739 gas] β”‚ β”‚ └── BasicSale.getDayEmission 233:248 [2445 gas] β”‚ β”œβ”€β”€ BasicSale._processWithdrawal 437:993 [-7726 / -616 gas] β”‚ β”‚ β”œβ”€β”€ BasicSale.getEmissionShare 484:859 [4956 / 6919 gas] β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └── MockERC20.balanceOf [STATICCALL] 616:738 [1963 gas] β”‚ β”‚ β”‚ β”œβ”€β”€ address: mockerc20.address β”‚ β”‚ β”‚ β”œβ”€β”€ input arguments: β”‚ β”‚ β”‚ β”‚ └── account: publicsale.address β”‚ β”‚ β”‚ └── return value: 100000000000000000000 β”‚ β”‚ β”‚ β”‚ β”‚ └── SafeMath.sub 924:984 [191 gas] β”‚ └── SafeMath.sub 1040:1100 [191 gas] β”‚ β”œβ”€β”€ MockERC20.transfer [CALL] 1269:1554 [1115 / 30109 gas] β”‚ β”‚ β”œβ”€β”€ address: mockerc20.address β”‚ β”‚ β”œβ”€β”€ value: 0 β”‚ β”‚ β”œβ”€β”€ input arguments: β”‚ β”‚ β”‚ β”œβ”€β”€ recipient: user2.address β”‚ β”‚ β”‚ └── amount: 27272727272727272727 β”‚ β”‚ └── return value: True β”‚ β”‚ β”‚ └── ERC20.transfer 1366:1534 [50 / 28994 gas] β”‚ └── ERC20._transfer 1374:1526 [28944 gas] └── Vesting.vest [CALL] 1705:3712 [-330491 / -303190 gas] β”‚ β”œβ”€β”€ address: vesting.address β”‚ β”œβ”€β”€ value: 0 β”‚ β”œβ”€β”€ input arguments: β”‚ β”‚ β”œβ”€β”€ _beneficiary: user2.address β”‚ β”‚ β”œβ”€β”€ _amount: 63636363636363636363 β”‚ β”‚ └── _isRevocable: 0 β”‚ └── revert reason: ERC20: transfer amount exceeds allowance <------------- β”‚ β”œβ”€β”€ SafeMath.add 1855:1883 [94 gas] β”œβ”€β”€ SafeMath.add 3182:3210 [94 gas] β”œβ”€β”€ SafeMath.add 3236:3264 [94 gas] β”‚ └── MockERC20.transferFrom [CALL] 3341:3700 [99923 / 27019 gas] β”‚ β”œβ”€β”€ address: mockerc20.address β”‚ β”œβ”€β”€ value: 0 β”‚ β”œβ”€β”€ input arguments: β”‚ β”‚ β”œβ”€β”€ sender: publicsale.address β”‚ β”‚ β”œβ”€β”€ recipient: vesting.address β”‚ β”‚ └── amount: 63636363636363636363 β”‚ └── revert reason: ERC20: transfer amount exceeds allowance β”‚ └── ERC20.transferFrom 3465:3700 [-97648 / -72904 gas] └── ERC20._transfer 3473:3625 [24744 gas]

Tools Used

Manual testing

The following approve() call should be added in the constructor of the BasicSale contract: mainToken.approve(address(vestLock), 2**256-1);

Findings Information

🌟 Selected for report: Reigada

Also found by: 0v3rf10w, Ruhum, WatchPug, cmichel, defsec, loop, pauliax

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

52.1514 USDC - $52.15

External Links

Handle

Reigada

Vulnerability details

Impact

Multiple calls to transferFrom and transfer are frequently done without checking the results. For certain ERC20 tokens, if insufficient tokens are present, no revert occurs but a result of β€œfalse” is returned. It’s important to check this. If you don’t, in this concrete case, some airdrop eligible participants could be left without their tokens. It is also a best practice to check this.

Proof of Concept

AirdropDistributionMock.sol:132: mainToken.transfer(msg.sender, claimable_to_send); AirdropDistributionMock.sol:157: mainToken.transfer(msg.sender, claimable_to_send); AirdropDistribution.sol:542: mainToken.transfer(msg.sender, claimable_to_send); AirdropDistribution.sol:567: mainToken.transfer(msg.sender, claimable_to_send);

InvestorDistribution.sol:132: mainToken.transfer(msg.sender, claimable_to_send); InvestorDistribution.sol:156: mainToken.transfer(msg.sender, claimable_to_send); InvestorDistribution.sol:207: mainToken.transfer(msg.sender, bal);

Vesting.sol:95: vestingToken.transferFrom(msg.sender, address(this), _amount);

PublicSale.sol:224: mainToken.transfer(_member, v_value);

Tools Used

Manual testing

Check the result of transferFrom and transfer. Although if this is done, the contracts will not be compatible with non standard ERC20 tokens like USDT. For that reason, I would rather recommend making use of SafeERC20 library: safeTransfer and safeTransferFrom.

Findings Information

🌟 Selected for report: 0v3rf10w

Also found by: Reigada

Labels

bug
duplicate
2 (Med Risk)

Awards

392.5282 USDC - $392.53

External Links

Handle

Reigada

Vulnerability details

Impact

The return value of these low-level calls are not checked, so if the call fails, the Ether will be locked in the contract. Setting the risk as medium as the smart contract has no function to withdraw the Ether. This Ether would remain stuck in the contract forever.

BasicSale.receive() (contracts/PublicSale.sol#148-156) ignores return value by burnAddress.call{value: msg.value}() (contracts/PublicSale.sol#154) BasicSale.burnEtherForMember(address) (contracts/PublicSale.sol#158-166) ignores return value by burnAddress.call{value: msg.value}() (contracts/PublicSale.sol#164)

Proof of Concept

https://github.com/code-423n4/2021-11-bootfinance/blob/main/tge/contracts/PublicSale.sol#L154 https://github.com/code-423n4/2021-11-bootfinance/blob/main/tge/contracts/PublicSale.sol#L164

Tools Used

Slither

Add a require statement that checks that the low-level call was successful.

#0 - chickenpie347

2021-11-16T14:08:09Z

Addressed in #145

Findings Information

🌟 Selected for report: defsec

Also found by: Reigada, Ruhum, elprofesor, mics, pants, pauliax

Labels

bug
duplicate
2 (Med Risk)

Awards

85.8459 USDC - $85.85

External Links

Handle

Reigada

Vulnerability details

Impact

The contract InvestorDistribution does not follow the transfer ownership pattern. The current ownership transfer process involves the current admin calling InvestorDistribution.setAdmin(). This function checks that the caller is the current admin and that the new admin is not the zero address and proceeds to write the new admin’s address into the admin’s state variable. If the nominated EOA account is not a valid account, it is entirely possible the admin may accidentally transfer ownership to an uncontrolled account. That account would be able to call dev_rugpull() (after 5 years) claiming the unclaimed tokens. On the other hand, it is also possible and more likely that the new account does not realize the permissions gotten over the smart contract and that the unclaimed tokens remain in the contract stuck forever.

Proof of Concept

https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L212

Tools Used

Manual testing

Consider implementing a two step process where the admin/owner nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.

#0 - chickenpie347

2022-01-03T21:16:32Z

Duplicate of #35

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter