From c706f3201b2f87cfb323f827951b54993ac9f419 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 25 Sep 2024 09:21:09 +0200 Subject: [PATCH] PUFI -> PUFFER --- mainnet-contracts/src/PUFFER.sol | 124 ++++++++++++++++++++++++ mainnet-contracts/src/PUFI.sol | 47 --------- mainnet-contracts/test/unit/PUFER.t.sol | 84 ++++++++++++++++ mainnet-contracts/test/unit/PUFI.t.sol | 49 ---------- 4 files changed, 208 insertions(+), 96 deletions(-) create mode 100644 mainnet-contracts/src/PUFFER.sol delete mode 100644 mainnet-contracts/src/PUFI.sol create mode 100644 mainnet-contracts/test/unit/PUFER.t.sol delete mode 100644 mainnet-contracts/test/unit/PUFI.t.sol diff --git a/mainnet-contracts/src/PUFFER.sol b/mainnet-contracts/src/PUFFER.sol new file mode 100644 index 0000000..377c064 --- /dev/null +++ b/mainnet-contracts/src/PUFFER.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { ERC20Votes } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { Time } from "@openzeppelin/contracts/utils/types/Time.sol"; +import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; +import { Votes } from "@openzeppelin/contracts/governance/utils/Votes.sol"; + +/** + * @title PUFFER Token + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + */ +contract PUFFER is ERC20Votes, ERC20Permit, Pausable, Ownable { + /** + * @notice Thrown when a transfer is attempted while the token is paused + */ + error TransferPaused(); + + /** + * @notice Event emitted when the allowedFrom status of an address is set + * @param from The address that is allowed to transfer tokens + * @param isAllowedFrom Whether the address is allowed to transfer tokens + */ + event SetAllowedFrom(address indexed from, bool isAllowedFrom); + + /** + * @notice Event emitted when the allowedTo status of an address is set + * @param to The address that is allowed to receive tokens + * @param isAllowedTo Whether the address is allowed to receive tokens + */ + event SetAllowedTo(address indexed to, bool isAllowedTo); + + /** + * @notice Mapping of addresses that are allowed to transfer tokens + * @dev This is used to allow certain addresses to transfer tokens without pausing the token + */ + mapping(address sender => bool allowed) public isAllowedFrom; + + /** + * @notice Mapping of addresses that are allowed to receive tokens + * @dev This is used to allow certain addresses to receive tokens without pausing the token + */ + mapping(address receiver => bool allowed) public isAllowedTo; + + /** + * @notice Constructor for the PUFI token + * totalSupply is 1 billion PUFI + */ + constructor(address initialOwner) ERC20("PUFFER", "PUFFER") ERC20Permit("PUFFER") Ownable(initialOwner) { + _mint(initialOwner, 1_000_000_000 ether); + _setAllowedFrom(initialOwner, true); + _pause(); + } + + /** + * @inheritdoc Votes + */ + function clock() public view virtual override returns (uint48) { + return Time.timestamp(); + } + + /** + * @inheritdoc Votes + */ + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } + + /** + * @inheritdoc ERC20Permit + */ + function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } + + /** + * @notice Unpauses the token + * @dev Only the owner can unpause the token + */ + function unpause() external virtual onlyOwner { + _unpause(); + } + + /** + * @notice Sets the allowedTo status of an address + * @param receiver The address to set the allowedTo status of + * @param allowed Whether the address is allowed to receive tokens + */ + function setAllowedTo(address receiver, bool allowed) external onlyOwner { + isAllowedTo[receiver] = allowed; + emit SetAllowedTo(receiver, allowed); + } + + /** + * @notice Sets the allowedFrom status of an address + * @param transferrer The address to set the allowedFrom status of + * @param allowed Whether the address is allowed to transfer tokens + */ + function setAllowedFrom(address transferrer, bool allowed) external onlyOwner { + _setAllowedFrom(transferrer, allowed); + } + + function _setAllowedFrom(address transferrer, bool allowed) internal { + isAllowedFrom[transferrer] = allowed; + emit SetAllowedFrom(transferrer, allowed); + } + + /** + * @notice Overrides the _update function to prevent token transfers + * @dev We override the _update function to act like `_beforeTokenTransfer` hook + */ + function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) { + if (paused()) { + require(isAllowedFrom[from] || isAllowedTo[to], TransferPaused()); + } + + super._update(from, to, value); + } +} diff --git a/mainnet-contracts/src/PUFI.sol b/mainnet-contracts/src/PUFI.sol deleted file mode 100644 index 3cf569d..0000000 --- a/mainnet-contracts/src/PUFI.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -/** - * @title PufferProtocol - * @author Puffer Finance - * @custom:security-contact security@puffer.fi - */ -contract PUFI is ERC20, ERC20Permit, Pausable, Ownable { - /** - * @notice Thrown when a transfer is attempted while the token is paused - */ - error PUFITransferPaused(); - - /** - * @notice Constructor for the PUFI token - * totalSupply is 1 billion PUFI - */ - constructor(address initialOwner) ERC20("PUFI", "PUFI") ERC20Permit("PUFI") Ownable(initialOwner) { - _mint(initialOwner, 1_000_000_000 ether); - - _pause(); - } - - /** - * @notice Unpauses the token - * @dev Only the owner can unpause the token - */ - function unpause() external virtual onlyOwner { - _unpause(); - } - - /** - * @notice Overrides the _update function to prevent token transfers - * @dev We override the _update function to act like `_beforeTokenTransfer` hook - */ - function _update(address from, address to, uint256 value) internal override { - require(!paused() || owner() == _msgSender(), PUFITransferPaused()); - - super._update(from, to, value); - } -} diff --git a/mainnet-contracts/test/unit/PUFER.t.sol b/mainnet-contracts/test/unit/PUFER.t.sol new file mode 100644 index 0000000..10a0998 --- /dev/null +++ b/mainnet-contracts/test/unit/PUFER.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { PUFFER } from "../../src/PUFFER.sol"; +import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; + +contract MockLocker { +// do nothing +} + +contract PUFFERTest is UnitTestHelper { + address owner = makeAddr("multisig"); + PUFFER public puffer; + MockLocker public locker; + + function setUp() public override { + puffer = new PUFFER(owner); + locker = new MockLocker(); + } + + function test_constructor() public { + puffer = new PUFFER(owner); + assertEq(puffer.owner(), owner); + assertEq(puffer.totalSupply(), 1_000_000_000 ether); + assertEq(puffer.name(), "PUFFER"); + assertEq(puffer.symbol(), "PUFFER"); + assertEq(puffer.paused(), true, "PUFFER should be paused"); + assertEq(puffer.CLOCK_MODE(), "mode=timestamp", "Clock mode must be timestamp"); + assertEq(puffer.nonces(owner), 0, "Nonce should be 0"); + } + + function test_allowedSenderCanTransferToAnybody(address recipient) public { + vm.assume(recipient != address(0)); + assertEq(puffer.paused(), true, "PUFFER should be paused"); + + vm.startPrank(owner); + puffer.transfer(alice, 100 ether); + puffer.setAllowedFrom(alice, true); + + vm.startPrank(alice); + + puffer.transfer(recipient, 100 ether); + + assertEq(puffer.balanceOf(recipient), 100 ether); + } + + function test_allowedRecipientCanReceiveTokensFromAnybody() public { + assertEq(puffer.paused(), true, "PUFFER should be paused"); + + vm.startPrank(owner); + puffer.transfer(alice, 100 ether); + puffer.setAllowedTo(address(locker), true); + + vm.startPrank(alice); + puffer.transfer(address(locker), 100 ether); + assertEq(puffer.balanceOf(address(locker)), 100 ether); + } + + function test_onlyOwnerCanTransfer() public { + uint256 amount = 100 ether; + assertEq(puffer.balanceOf(alice), 0, "Alice should have 0 PUFFER"); + vm.prank(owner); + puffer.transfer(alice, amount); + assertEq(puffer.balanceOf(alice), amount, "Alice should have 100 PUFFER"); + + vm.startPrank(alice); + vm.expectRevert(PUFFER.TransferPaused.selector); + puffer.transfer(bob, amount); + } + + // Token transfer should work when the token is unpaused + function test_unpausedTokenTransfer(uint80 aliceAmount) public { + vm.assume(aliceAmount > 0); + + vm.startPrank(owner); + puffer.transfer(alice, aliceAmount); + + puffer.unpause(); + + vm.startPrank(alice); + puffer.transfer(bob, aliceAmount); + assertEq(puffer.balanceOf(bob), aliceAmount, "Bob should have 100 PUFFER"); + } +} diff --git a/mainnet-contracts/test/unit/PUFI.t.sol b/mainnet-contracts/test/unit/PUFI.t.sol deleted file mode 100644 index 8f48a1c..0000000 --- a/mainnet-contracts/test/unit/PUFI.t.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -import { PUFI } from "../../src/PUFI.sol"; -import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; - -contract PUFITest is UnitTestHelper { - address owner = makeAddr("multisig"); - PUFI public pufi; - - function setUp() public override { - pufi = new PUFI(owner); - } - - function test_constructor() public { - pufi = new PUFI(owner); - assertEq(pufi.owner(), owner); - assertEq(pufi.totalSupply(), 1_000_000_000 ether); - assertEq(pufi.name(), "PUFI"); - assertEq(pufi.symbol(), "PUFI"); - } - - // Only owner can transfer tokens while the token is paused - function test_onlyOwnerCanTransfer() public { - uint256 amount = 100 ether; - assertEq(pufi.balanceOf(alice), 0, "Alice should have 0 PUFI"); - vm.prank(owner); - pufi.transfer(alice, amount); - assertEq(pufi.balanceOf(alice), amount, "Alice should have 100 PUFI"); - - vm.startPrank(alice); - vm.expectRevert(PUFI.PUFITransferPaused.selector); - pufi.transfer(bob, amount); - } - - // Token transfer should work when the token is unpaused - function test_unpausedTokenTransfer(uint80 aliceAmount) public { - vm.assume(aliceAmount > 0); - - vm.startPrank(owner); - pufi.transfer(alice, aliceAmount); - - pufi.unpause(); - - vm.startPrank(alice); - pufi.transfer(bob, aliceAmount); - assertEq(pufi.balanceOf(bob), aliceAmount, "Bob should have 100 PUFI"); - } -}