Conversation
src/vlPUFFER.sol
Outdated
| // 1% in basis points | ||
| uint256 internal constant _KICKER_FEE_BPS = 100; | ||
| // 10000 in basis points | ||
| uint256 internal constant _KICKER_FEE_DENOMINATOR = 10000; |
There was a problem hiding this comment.
When writing numbers in basis points I like to write them like this 100_00 for clarity. I don't really mind, just a suggestion
There was a problem hiding this comment.
No, no. I mean 100_00. And 1_00 for _KICKER_FEE_BPS. It's like indicating 100.00% or 1.00%. Since it's basis points the last 2 positions are decimals. 10_000 is even more confusing :)
There was a problem hiding this comment.
I've added a configuration so that it automatically formats thousands when you do forge fmt
number_underscore = "thousands"
There was a problem hiding this comment.
That's a good config, but for this case: meh. We should add a // forgefmt: disable-next-line because that's a confusing way to write basis points
src/vlPUFFER.sol
Outdated
| uint256 pufferAmount, | ||
| uint256 pufferAmountAdded, | ||
| uint256 unlockTime, | ||
| uint256 vlPUFFERAmount |
There was a problem hiding this comment.
Since we're showing the increase in pufferAmount, should we do the same in vlPUFFERAmount? Not sure about this since it can be calculated from the data of the event + the timestamp
There was a problem hiding this comment.
Or maybe remove it completely?, There are transfer events in the PUFFER token, we can figure it out if needed, also it can be read from the calldata. Maybe we don't need it in the event at all pufferAmountAdded wyt?
There was a problem hiding this comment.
Hmm from a BE it can be calculated without problem, but for manual inspection of a tx in the explorer the info in the event might be useful. I'd rather still have it but I don't really mind if it's removed. Your call
| * @notice Withdraw the tokens | ||
| * @param recipient Address to receive the PUFFER tokens | ||
| */ | ||
| function withdraw(address recipient) external { |
There was a problem hiding this comment.
Do we want to implement an emergencyWithdraw function or nah? As I understand it, it would be just like this but ignoring the TokensLocked check and only whenPaused
There was a problem hiding this comment.
We should have a discussion on this. Without additional context, its really useless. We need to plan for the future and figure out if ti makes sense to add it.
src/vlPUFFER.sol
Outdated
| function _calculateUnlockTimeAndMultiplier(uint256 unlockTime) | ||
| internal | ||
| view | ||
| returns (uint256 roundedUnlockTime, uint256 multiplier) | ||
| { | ||
| multiplier = ((unlockTime - block.timestamp) / _LOCK_TIME_MULTIPLIER); | ||
| // Round down the unlockTime to the nearest 30-day multiplier | ||
| roundedUnlockTime = block.timestamp + (multiplier * _LOCK_TIME_MULTIPLIER); | ||
| } |
There was a problem hiding this comment.
So if user tries to lock for 59 days he will get 30 days instead. This is unintuitive and unexpected I'd say. I'd suggest instead of unlock time in create lock we can ask for the multiplier directly (between 1 and 24). You can call it multiplier or months
There was a problem hiding this comment.
Correct. The frontend will always use a bigger timestamp to achieve the desired result.
Scenario:
User is on our frontend, and wants to lock for 1 year = x12 multiplier. When the frontend prepares the transaction, the timestamp will be:
unlockTImestamp = now + 1 year in seconds + 30 days in seconds (if a user uses really low gas price, and it takes a few days for the transaction to be included). This will round down in the smart contract, and the user will get the desired multiplier.
It can be refactored to use multiplier instead of the unlockTime. We should also have a sync with the business team before auditing the code.
src/vlPUFFER.sol
Outdated
| function createLockWithPermit(uint256 value, uint256 unlockTime, uint256 deadline, uint8 v, bytes32 r, bytes32 s) | ||
| external | ||
| { | ||
| IERC20Permit(address(PUFFER)).permit(msg.sender, address(this), value, deadline, v, r, s); |
There was a problem hiding this comment.
where is the transfer of PUFFER after permit? bug?
src/vlPUFFER.sol
Outdated
| * @param r First 32 bytes of the signature | ||
| * @param s Second 32 bytes of the signature | ||
| */ | ||
| function createLockWithPermit(uint256 value, uint256 unlockTime, uint256 deadline, uint8 v, bytes32 r, bytes32 s) |
There was a problem hiding this comment.
i would suggest to use this pattern for permit as we are already following this pattern on our frontend:
Permit calldata permitData
and then:
permitData.v, ....
and also use try() catch() for permit so that we don't get internal error on etherscan.
src/vlPUFFER.sol
Outdated
| multiplier = ((unlockTime - block.timestamp) / _LOCK_TIME_MULTIPLIER); | ||
| // Round down the unlockTime to the nearest 30-day multiplier | ||
| roundedUnlockTime = block.timestamp + (multiplier * _LOCK_TIME_MULTIPLIER); |
There was a problem hiding this comment.
auditors might raise issue of underflow/overflow here. and suggest to use SafeMath and checks
There was a problem hiding this comment.
i did cast block timestamp to uint256, to avoid this issue. good catch
| uint256 internal constant _KICKER_FEE_DENOMINATOR = 10_000; | ||
| // 100% in basis points | ||
| // forgefmt: disable-next-line | ||
| uint256 internal constant _KICKER_FEE_DENOMINATOR = 100_00; |
There was a problem hiding this comment.
100_00 ? it should be 10_000
There was a problem hiding this comment.
actually we don't even need this as a constant. the term BPS already implies 10,000
There was a problem hiding this comment.
true, but since it is not upgradeable contract, and we are hardcoding it. it might as well be a readable. this suggestion was made by @eladiosch because 10k bps = 100 %, so i just followed up on it
src/vlPUFFER.sol
Outdated
| // User may deposit more tokens to old lock, or extend the lock | ||
| require(unlockTime >= lockInfo.unlockTime, UnlockTimeMustBeGreaterOrEqualThanLock()); | ||
| require(lockInfo.pufferAmount > 0, LockDoesNotExist()); | ||
|
|
||
| (uint256 roundedUnlockTime, uint256 multiplier) = _calculateUnlockTimeAndMultiplier(unlockTime); |
There was a problem hiding this comment.
what if rounded unlock time is the same as unlock time?
| require(lockInfo.pufferAmount > 0, LockDoesNotExist()); | ||
| require(lockInfo.unlockTime <= block.timestamp, TokensLocked()); | ||
|
|
There was a problem hiding this comment.
inconsistent usage of require + revert everywhere, you should pick one and use that only
There was a problem hiding this comment.
IMO, we don't need to stay consistent with require vs revert. In some cases, it is more readable to have it in require, while on the others, if () revert makes a little more sense.
| PUFFER.safeTransferFrom(msg.sender, address(this), amount); | ||
| } | ||
|
|
||
| LockInfo memory lockInfo = lockInfos[msg.sender]; |
There was a problem hiding this comment.
No need to copy this to memory, you can make it a storage variable and also use this variable for the assignment on line 255
| _burn(user, vlPUFFERAmount); | ||
|
|
||
| // Send the rest of the PUFFER tokens to the user | ||
| PUFFER.safeTransfer(user, pufferAmount - kickerFee); |
There was a problem hiding this comment.
what if user can't recover ERC20
There was a problem hiding this comment.
The user was the one who initially locked ERC20 tokens. This makes me think, if a user can use 7702 to revert this transaction, making it impossible to get kicked. I'll research this topic a little more, before we finalize the contract.
There was a problem hiding this comment.
I see how usign 7702 user could prevent native token from being received. However, this is a ERC20 transfer. Unless our Puffer token tried to call an onTokensReceived hook like ERC777, I don't think user can make the kick tx revert. The only case I can think regarding Tim's comment is the the user is a smart contract without a function to recover ERC20. And about that case, we should just inform about the kick flow so if a contract wants to use this protocol, they should implement a way to recover ERC20
There was a problem hiding this comment.
Yeah, I wrote a test to verify that.
src/vlPUFFER.sol
Outdated
| roundedUnlockTime = uint256(block.timestamp) + (multiplier * _LOCK_TIME_MULTIPLIER); | ||
| } | ||
|
|
||
| function _createLock(uint256 amount, uint256 unlockTime) internal onlyValidLockDuration(unlockTime) whenNotPaused { |
There was a problem hiding this comment.
getting multiplier as param directly instead of unlock time would make it more readable since there would be no uncertainty or mental calculations
| uint256 internal constant _KICKER_FEE_DENOMINATOR = 10_000; | ||
| // 100% in basis points | ||
| // forgefmt: disable-next-line | ||
| uint256 internal constant _KICKER_FEE_DENOMINATOR = 100_00; |
There was a problem hiding this comment.
actually we don't even need this as a constant. the term BPS already implies 10,000
ksatyarth2
left a comment
There was a problem hiding this comment.
All LGTM now! Just a suggestion to add fuzz tests considering our last audit
|
@tdenisenko Pls review |
No description provided.