Summary
The getPrice()
function is used to get the current price of ZENO
tokens in USDC
. But per its current logic, it will always round up to the nearest whole number, even on values that are very negligible.
PoC
This is the current logic used to get the slippage price of tokens:
function getPrice() public view returns (uint256) {
...
...
return state.startingPrice
- (
(state.startingPrice - state.reservePrice) * (block.timestamp - state.startTime)
/ (state.endTime - state.startTime)
);
}
If we run the following test, we can see the current slippage price:
pragma solidity ^0.8.19;
import {Test, console2} from "forge-std/Test.sol";
import {Auction} from "../contracts/zeno/Auction.sol";
import {ZENO} from "../contracts/zeno/ZENO.sol";
import {MockUSDC} from "../contracts/mocks/core/tokens/MockUSDC.sol";
contract AuctionTest is Test {
Auction auction;
ZENO zeno;
MockUSDC usdc;
address zenoOwner = address(0x1);
address businessAddress = address(0x2);
address buyer = address(0x3);
address auctionOwner = address(0x4);
uint256 startTime = 3600 seconds;
uint256 endTime = startTime + 3600 seconds;
uint256 startingPrice = 1e18;
uint256 reservePrice = 5e17;
uint256 totalAllocated = 1000e18;
uint256 initialSupply = 10_000e18;
uint256 zenoMaturityDate = 1 hours;
function setUp() public {
usdc = new MockUSDC(initialSupply);
zeno = new ZENO(address(usdc), zenoMaturityDate, "ZTOKEN", "ZTK", zenoOwner);
auction = new Auction(
address(zeno),
address(usdc),
businessAddress,
startTime,
endTime,
startingPrice,
reservePrice,
totalAllocated,
auctionOwner
);
usdc.mint(buyer, 1000e18);
}
function testGetPrice() public {
console2.log(auction.getPrice());
vm.warp(4600 seconds);
console2.log(auction.getPrice());
}
}
But if we calculate the current slippage price using the logic in the getPrice()
function:
1e18 - ((1e18 - 5e17) * (4600 - 3600) / (7200 - 3600))
We get:
861,111,111,111,111,111.11111111111111
The current logic rounds up even on very negligible amounts.
Impact
ZENO
tokens could be listed at prices that would result in slippage prices becoming quite high. And because the getPrice()
logic rounds up every amount, the buyer could end up paying significantly higher than expected.
Tools Used
Recommendations
I added an internal function that only rounds up on amounts greater than .4
, .1 - .4
rounds down.
function divideAndRound(uint256 numerator, uint256 denominator) internal pure returns (uint256) {
require(denominator > 0, "Division by zero or negative values not allowed!!!");
uint256 quotient = numerator / denominator;
uint256 remainder = numerator % denominator;
if ((remainder * 2) >= denominator) {
quotient += 1;
}
return quotient;
}
Then this internal function is used to calculate the current slippage price in getPrice()
.
function getPrice() public view returns (uint256) {
...
...
- return state.startingPrice
- - (
- (state.startingPrice - state.reservePrice) * (block.timestamp - state.startTime)
- / (state.endTime - state.startTime)
- );
+ uint256 priceDifference = state.startingPrice - state.reservePrice;
+ uint256 timeElapsed = block.timestamp - state.startTime;
+ uint256 totalDuration = state.endTime - state.startTime;
+
+ uint256 product = priceDifference * timeElapsed;
+ uint256 quotient = divideAndRound(product, totalDuration);
+ uint256 price = state.startingPrice - quotient;
+ return price;
}
If we run our test we would get a round down result:
function testGetPrice() public {
console2.log(auction.getPrice());
vm.warp(4600 seconds);
console2.log(auction.getPrice());
}
And it also works for round up results when appropriate.