From 6da4184d251e787115dcf40528ffaa4e656fbbe8 Mon Sep 17 00:00:00 2001 From: Joseph Schiarizzi <9449596+cupOJoseph@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:00:40 -0400 Subject: [PATCH 1/2] add admin --- src/ShellToken.sol | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ShellToken.sol b/src/ShellToken.sol index ae466e4..4edf859 100644 --- a/src/ShellToken.sol +++ b/src/ShellToken.sol @@ -6,16 +6,21 @@ import "openzeppelin-contracts/contracts/access/Ownable.sol"; contract ShellToken is ERC20, Ownable { + mapping(address => bool) public isAdmin; + mapping(address => uint) public multipler; //percentage out of 100 + mapping(address => bool) public allowedToTransfer; constructor() ERC20("ShellToken", "SHELL") Ownable(msg.sender) { } - function mintShells(address to, uint256 amount) public onlyOwner { + function mintShells(address to, uint256 amount) public { + require(isAdmin[msg.sender], "Not an admin"); _mint(to, amount); } - function deleteShells(address from, uint256 amount) public onlyOwner { + function deleteShells(address from, uint256 amount) public { + require(isAdmin[msg.sender], "Not an admin"); _burn(from, amount); } @@ -29,5 +34,9 @@ contract ShellToken is ERC20, Ownable { function updateAllowedToTransfer(address user, bool allowed) public onlyOwner { allowedToTransfer[user] = allowed; } + + function updateIsAdmin(address user, bool _isAdmin) public onlyOwner { + isAdmin[user] = _isAdmin; + } } From 2008440f8a493f8c9b9fea4915e125faac0a52d5 Mon Sep 17 00:00:00 2001 From: geovgy <54918343+geovgy@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:17:58 -0400 Subject: [PATCH 2/2] Add unit tests, batch mint, multiplier view/set funcs, + override transferFrom --- src/ShellToken.sol | 57 ++++++++-- test/ShellToken.t.sol | 243 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 test/ShellToken.t.sol diff --git a/src/ShellToken.sol b/src/ShellToken.sol index 4edf859..aebd643 100644 --- a/src/ShellToken.sol +++ b/src/ShellToken.sol @@ -6,31 +6,72 @@ import "openzeppelin-contracts/contracts/access/Ownable.sol"; contract ShellToken is ERC20, Ownable { + struct Recipient { + address to; + uint256 amount; + } + + struct Multiplier { + address activity; + uint256 multiplier; + } + mapping(address => bool) public isAdmin; - mapping(address => uint) public multipler; //percentage out of 100 + + // address of activity -> multiplier + // Activities are the contract addresses of Trove Managers, Stability Pools, LP tokens, etc. + mapping(address => uint) public multiplier; //percentage out of 100 mapping(address => bool) public allowedToTransfer; - constructor() ERC20("ShellToken", "SHELL") Ownable(msg.sender) { - } + constructor() ERC20("ShellToken", "SHELL") Ownable(msg.sender) {} function mintShells(address to, uint256 amount) public { require(isAdmin[msg.sender], "Not an admin"); _mint(to, amount); } + function mintBatchShells(Recipient[] calldata recipients) public { + require(isAdmin[msg.sender], "Not an admin"); + for (uint i; i < recipients.length; ) { + _mint(recipients[i].to, recipients[i].amount); + unchecked { ++i; } + } + } + function deleteShells(address from, uint256 amount) public { require(isAdmin[msg.sender], "Not an admin"); _burn(from, amount); } function transfer(address to, uint256 amount) public override returns (bool) { - if (allowedToTransfer[msg.sender]) { - return super.transfer(to, amount); + if (!allowedToTransfer[msg.sender]) { + revert("Not allowed to transfer"); } - return false; + return super.transfer(to, amount); } + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + if (!allowedToTransfer[from]) { + revert("Not allowed to transfer"); + } + return super.transferFrom(from, to, amount); + } + + + function getMultipliers(address[] calldata contracts) external view returns (Multiplier[] memory) { + Multiplier[] memory multipliers = new Multiplier[](contracts.length); + for (uint i; i < contracts.length; ) { + multipliers[i] = Multiplier(contracts[i], multiplier[contracts[i]]); + unchecked { ++i; } + } + return multipliers; + } + + ////////////////////////// + // ONLY OWNER FUNCTIONS // + ////////////////////////// + function updateAllowedToTransfer(address user, bool allowed) public onlyOwner { allowedToTransfer[user] = allowed; } @@ -38,5 +79,9 @@ contract ShellToken is ERC20, Ownable { function updateIsAdmin(address user, bool _isAdmin) public onlyOwner { isAdmin[user] = _isAdmin; } + + function setMultiplier(address activity, uint perc) public onlyOwner { + multiplier[activity] = perc; + } } diff --git a/test/ShellToken.t.sol b/test/ShellToken.t.sol new file mode 100644 index 0000000..92ca1da --- /dev/null +++ b/test/ShellToken.t.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../src/ShellToken.sol"; +import "openzeppelin-contracts/contracts/access/Ownable.sol"; + +contract ShellTokenTest is Test { + ShellToken internal shellToken; + address internal admin = makeAddr("admin"); + address internal owner = makeAddr("owner"); + + function setUp() public { + vm.startPrank(owner); + shellToken = new ShellToken(); + shellToken.updateIsAdmin(admin, true); + vm.stopPrank(); + } + + function test_updateIsAdmin() public { + address tempAdmin = makeAddr("tempAdmin"); + + assertEq(shellToken.isAdmin(tempAdmin), false); + + vm.prank(owner); + shellToken.updateIsAdmin(tempAdmin, true); + + assertEq(shellToken.isAdmin(tempAdmin), true); + + vm.prank(owner); + shellToken.updateIsAdmin(tempAdmin, false); + + assertEq(shellToken.isAdmin(tempAdmin), false); + } + + function test_mintShells() public { + address user = makeAddr("user"); + + vm.startPrank(admin); + shellToken.mintShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 100); + } + + function test_revert_mintShells_notAdmin() public { + address user = makeAddr("user"); + + vm.startPrank(makeAddr("notAdmin")); + vm.expectRevert("Not an admin"); + shellToken.mintShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 0); + } + + function test_mintBatchShells() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + + ShellToken.Recipient[] memory recipients = new ShellToken.Recipient[](2); + recipients[0] = ShellToken.Recipient(user1, 100); + recipients[1] = ShellToken.Recipient(user2, 200); + + vm.startPrank(admin); + shellToken.mintBatchShells(recipients); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user1), 100); + assertEq(shellToken.balanceOf(user2), 200); + } + + function test_revert_mintBatchShells_notAdmin() public { + address user = makeAddr("user"); + + ShellToken.Recipient[] memory recipients = new ShellToken.Recipient[](1); + recipients[0] = ShellToken.Recipient(user, 100); + + vm.startPrank(makeAddr("notAdmin")); + vm.expectRevert("Not an admin"); + shellToken.mintBatchShells(recipients); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 0); + } + + function test_deleteShells() public { + address user = makeAddr("user"); + + vm.startPrank(admin); + shellToken.mintShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 100); + + vm.startPrank(admin); + shellToken.deleteShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 0); + } + + function test_revert_deleteShells_notAdmin() public { + address user = makeAddr("user"); + + vm.startPrank(admin); + shellToken.mintShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 100); + + vm.startPrank(makeAddr("notAdmin")); + vm.expectRevert("Not an admin"); + shellToken.deleteShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 100); + } + + function test_revert_transfer_notAllowed() public { + address user = makeAddr("user"); + address to = makeAddr("to"); + + vm.startPrank(admin); + shellToken.mintShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 100); + + vm.startPrank(user); + vm.expectRevert("Not allowed to transfer"); + shellToken.transfer(to, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 100); + assertEq(shellToken.balanceOf(to), 0); + } + + function test_transfer_allowed() public { + address user = makeAddr("user"); + address to = makeAddr("to"); + + vm.prank(admin); + shellToken.mintShells(user, 100); + + assertEq(shellToken.balanceOf(user), 100); + + vm.prank(owner); + shellToken.updateAllowedToTransfer(user, true); + + vm.prank(user); + shellToken.transfer(to, 100); + + assertEq(shellToken.balanceOf(user), 0); + assertEq(shellToken.balanceOf(to), 100); + } + + function test_revert_transferFrom_notAllowed() public { + address user = makeAddr("user"); + address to = makeAddr("to"); + + vm.startPrank(admin); + shellToken.mintShells(user, 100); + vm.stopPrank(); + + assertEq(shellToken.balanceOf(user), 100); + + vm.prank(user); + shellToken.approve(address(this), 100); + + vm.expectRevert("Not allowed to transfer"); + shellToken.transferFrom(user, to, 100); + + assertEq(shellToken.balanceOf(user), 100); + assertEq(shellToken.balanceOf(to), 0); + } + + function test_transferFrom_allowed() public { + address user = makeAddr("user"); + address to = makeAddr("to"); + + vm.prank(admin); + shellToken.mintShells(user, 100); + + assertEq(shellToken.balanceOf(user), 100); + + vm.prank(owner); + shellToken.updateAllowedToTransfer(user, true); + + vm.prank(user); + shellToken.approve(address(this), 100); + + shellToken.transferFrom(user, to, 100); + + assertEq(shellToken.balanceOf(user), 0); + assertEq(shellToken.balanceOf(to), 100); + } + + function test_setMultiplier() public { + address activity = makeAddr("activity"); + + vm.startPrank(owner); + shellToken.setMultiplier(activity, 100); + vm.stopPrank(); + } + + function test_revert_setMultiplier_notOwner() public { + address activity = makeAddr("activity"); + address notOwner = makeAddr("notOwner"); + + vm.startPrank(notOwner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, notOwner)); + shellToken.setMultiplier(activity, 100); + vm.stopPrank(); + + vm.startPrank(admin); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, admin)); + shellToken.setMultiplier(activity, 100); + vm.stopPrank(); + } + + function test_getMultipliers() public { + address[] memory contracts = new address[](3); + contracts[0] = makeAddr("activity1"); + contracts[1] = makeAddr("activity2"); + contracts[2] = makeAddr("activity3"); + + vm.startPrank(owner); + for (uint i; i < contracts.length; ) { + shellToken.setMultiplier(contracts[i], 100 + i); + unchecked { ++i; } + } + vm.stopPrank(); + + ShellToken.Multiplier[] memory multipliers = shellToken.getMultipliers(contracts); + assertEq(multipliers.length, contracts.length); + for (uint i; i < multipliers.length; ) { + assertEq(multipliers[i].activity, contracts[i]); + assertEq(multipliers[i].multiplier, 100 + i); + unchecked { ++i; } + } + } +} \ No newline at end of file