Skip to content

Conversation

@bxmmm1
Copy link
Contributor

@bxmmm1 bxmmm1 commented May 7, 2025

No description provided.

@bxmmm1 bxmmm1 self-assigned this May 7, 2025
@bxmmm1 bxmmm1 changed the base branch from dev to main May 7, 2025 13:19
@bxmmm1 bxmmm1 changed the base branch from main to dev May 7, 2025 13:19
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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean 10_000 ? :D

Copy link

@eladiosch eladiosch May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a configuration so that it automatically formats thousands when you do forge fmt

number_underscore = "thousands"

https://book.getfoundry.sh/reference/config/formatter?highlight=number_underscore%20%3D%20%22thousands%22#number_underscore

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Comment on lines 146 to 154
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Comment on lines 151 to 153
multiplier = ((unlockTime - block.timestamp) / _LOCK_TIME_MULTIPLIER);
// Round down the unlockTime to the nearest 30-day multiplier
roundedUnlockTime = block.timestamp + (multiplier * _LOCK_TIME_MULTIPLIER);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auditors might raise issue of underflow/overflow here. and suggest to use SafeMath and checks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i did cast block timestamp to uint256, to avoid this issue. good catch

@bxmmm1 bxmmm1 changed the title add vlPUFFER locker Create vlPUFFER May 15, 2025
uint256 internal constant _KICKER_FEE_DENOMINATOR = 10_000;
// 100% in basis points
// forgefmt: disable-next-line
uint256 internal constant _KICKER_FEE_DENOMINATOR = 100_00;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100_00 ? it should be 10_000

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually we don't even need this as a constant. the term BPS already implies 10,000

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Comment on lines 220 to 224
// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if rounded unlock time is the same as unlock time?

Comment on lines +275 to +277
require(lockInfo.pufferAmount > 0, LockDoesNotExist());
require(lockInfo.unlockTime <= block.timestamp, TokensLocked());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inconsistent usage of require + revert everywhere, you should pick one and use that only

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if user can't recover ERC20

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually we don't even need this as a constant. the term BPS already implies 10,000

Copy link

@ksatyarth2 ksatyarth2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All LGTM now! Just a suggestion to add fuzz tests considering our last audit

@bxmmm1 bxmmm1 requested a review from tdenisenko May 19, 2025 07:54
@bxmmm1
Copy link
Contributor Author

bxmmm1 commented May 20, 2025

@tdenisenko Pls review

@bxmmm1 bxmmm1 merged commit c64d833 into dev May 20, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants