# Contract BasicSale is missing an approve(address(vestLock), 2**256-1) call

BasicSale contract lacks approve() for vestLock, so withdrawShare always reverts on Vesting.vest transferFrom, permanently locking all mainToken sent to the contract.

- Payout: Audit contest
- Severity: high
- Program: Boot Finance
- Found by: Reigada
- Disclosed: 2022-01-21
- Write-up quality: 6/10
- Original: https://github.com/code-423n4/2021-11-bootfinance-findings/issues/135
- Library page: https://bugbountylibrary.com/r/c4%3A2021-11-bootfinance%3AH-01
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:source-review vuln:business-logic vuln:misconfiguration

**Target:** BasicSale / PublicSale sale contract and Vesting contract
**Root cause:** BasicSale never calls mainToken.approve(address(vestLock), 2**256-1) in its constructor, unlike AirdropDistribution and InvestorDistribution, so Vesting.vest's transferFrom(msg.sender, address(this), amount) fails with insufficient allowance.
**Steps:**
1. Compare BasicSale (PublicSale.sol) against AirdropDistribution.sol#L499 and InvestorDistribution.sol#L80 which both call mainToken.approve(address(vestLock), 2**256-1).
2. Note BasicSale._withdrawShare calls Vesting.vest(beneficiary, amount, isRevocable) at PublicSale.sol#L225.
3. Vesting.vest executes vestingToken.transferFrom(msg.sender, address(this), _amount) at Vesting.sol#L95.
4. Call publicsale.withdrawShare(1, 1, {'from': user2}) in Brownie; observe revert `ERC20: transfer amount exceeds allowance`.
5. Confirm the team's MockVesting.sol#L10 has an empty vest() with no transfer, hiding the bug in tests.
**How found:** Code-reading comparison: noticed sibling distribution contracts include an approve() to vestLock while BasicSale does not, then traced withdrawShare into Vesting.vest's transferFrom and reproduced the revert in Brownie. Also spotted the mock's empty vest() explaining why tests missed it.
**Exploitation:** Not an attacker-driven exploit; the flaw is a functional deadlock. Any user calling withdrawShare triggers Vesting.vest, whose transferFrom reverts for lack of allowance, so all mainToken sent to BasicSale is permanently stuck with no retrieval path.
**Insight:** When a contract interacts with a token-pulling sink (transferFrom-based vesting/staking), diff its constructor approvals against sibling contracts that use the same sink; and distrust mocks that stub out the sink's transfer logic, since they mask allowance bugs.
