29270 - [SC - High] The main functionality of the contract EarlyZER...
Submitted on Mar 12th 2024 at 22:40:12 UTC by @stiglitz for Boost | ZeroLend
Report ID: #29270
Report type: Smart Contract
Report severity: High
Target: https://github.com/zerolend/governance
Impacts:
Protocol insolvency
Protocol contract does not work
Description
Brief/Intro
The function EarlyZEROVesting::startVesting
is broken due to missing allowances between contracts.
Vulnerability Details
The problem is in the following function call in the startVesting
function:
uint256 id = vesting.mint(
stake ? address(this) : msg.sender, // address _who,
(amount * 75) / 100, // uint256 _pending,
(amount * 25) / 100, // uint256 _upfront,
86400 * 30 * 3, // uint256 _linearDuration,
86400 * 30, // uint256 _cliffDuration,
block.timestamp, // uint256 _unlockDate,
false, // bool _hasPenalty
IVestedZeroNFT.VestCategory.EARLY_ZERO
);
Inside the VestedZeroNFT::mint
, almost the last line contains the following call:
zero.transferFrom(msg.sender, address(this), _pending + _upfront);
Where msg.sender == EarlyZEROVesting
.
Because there is no allowance set from == EarlyZEROVesting
to spender == VestedZeroNFT
, the TX will always revert (IERC20Errors.ERC20InsufficientBalance
)
This approval is necessary for the function EarlyZEROVesting::startVesting
:
function startVesting(uint256 amount, bool stake) external {
require(enableVesting || stake, "vesting not enabled; staking only");
earlyZERO.burnFrom(msg.sender, amount);
// Approve call here is necessary
earlyZERO.approve(address(vesting),amount);
...
Impact Details
The main functionality of the contract EarlyZEROVesting
, which is startVesting
is broken and always reverts
References
PoC and .png shows the problem
Proof of Concept
Test
tests/test_vested.py
from wake.testing import *
from pytypes.openzeppelin.contracts.proxy.ERC1967.ERC1967Proxy import ERC1967Proxy
from pytypes.contracts.locker.OmnichainStaking import OmnichainStaking
from pytypes.contracts.locker.LockerToken import LockerToken
from pytypes.contracts.locker.LockerLP import LockerLP
from pytypes.tests.VeToken import VeToken
from pytypes.contracts.vesting.earlyzero.EarlyZEROVesting import EarlyZEROVesting
from pytypes.contracts.vesting.earlyzero.EarlyZERO import EarlyZERO
from pytypes.contracts.vesting.VestedZeroNFT import VestedZeroNFT
from pytypes.contracts.vesting.StakingBonus import StakingBonus
'''
Test written in Wake testing framework (https://getwake.io/) aka boosted brownie
Docs:
https://ackeeblockchain.com/wake/docs/latest/
Repo:
https://github.com/Ackee-Blockchain/wake
How to run this test:
Install wake
$ pip install eth-wake
To have actual anvil version
$ foundryup
After installing project dependencies initialize wake
It will create `tests` folder and process foundry remappings if any
$ wake up
Generate python representation of contracts
$ wake init pytypes
Go to wake `tests` folder and paste this code in tests/test_vested.py and run
$ wake test tests/test_vested.py
'''
def deploy_with_proxy(contract):
impl = contract.deploy()
proxy = ERC1967Proxy.deploy(impl, b"")
return contract(proxy)
# Print failing tx call trace
def revert_handler(e: TransactionRevertedError):
if e.tx is not None:
print(e.tx.call_trace)
print(e.tx.events)
@default_chain.connect()
@on_revert(revert_handler)
def test_vested():
# ======================DEPLOY========================= #
random = default_chain.accounts[9]
owner = default_chain.accounts[0]
bob = default_chain.accounts[1]
# Deploy mock token
zero_token = EarlyZERO.deploy(from_=owner)
ve_token = VeToken.deploy(100*10**18, from_=bob)
# Proxy deployment
zero_vesting = deploy_with_proxy(EarlyZEROVesting)
omnichain = deploy_with_proxy(OmnichainStaking)
staking_bonus = deploy_with_proxy(StakingBonus)
vested_zero_nft = deploy_with_proxy(VestedZeroNFT)
locker_lp = deploy_with_proxy(LockerLP)
locker_token = deploy_with_proxy(LockerToken)
# Init deployment
zero_vesting.init(zero_token, vested_zero_nft, staking_bonus, from_=owner)
omnichain.init(random, locker_token, locker_lp, from_=owner)
staking_bonus.init(zero_token, locker_token, vested_zero_nft, 100, from_=owner)
vested_zero_nft.init(zero_token, staking_bonus, from_=owner)
locker_lp.init(ve_token, omnichain, random, from_=owner)
locker_token.init(ve_token, omnichain, random, from_=owner)
# Send something to bob
zero_token.transfer(bob, 100*10**18, from_=owner)
zero_token.transfer(zero_vesting, 100*10**18, from_=owner)
# Disable whitelist and blacklist
zero_token.toggleWhitelist(False, False, from_=owner)
zero_token.approve(zero_vesting, 100*10**18, from_=bob)
print(vested_zero_nft.address)
print(zero_vesting.address)
#zero_token.approve(vested_zero, 100*10**18, from_=zero_vesting)
zero_vesting.toggleVesting(from_=owner)
zero_vesting.startVesting(100*10**18, True, from_=bob)
# Just simply run test
# On-revert handler will print call trace and errors of the reverting TX
Last updated
Was this helpful?