Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 3 additions & 33 deletions src/unit/ERC20Mintable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,27 +91,9 @@ contract ERC20Mintable is IERC20Mintable, Ownable2Step, ERC20Permit {
* @custom:security Owner-only access prevents unauthorized deflation
*/
function burn(address from, address spender, uint256 amount) external onlyOwner {
_burn(from, spender, amount);
}

/**
* @notice Burns tokens from the specified address, decreasing total supply.
* @dev Callable by any address. Decreases both total supply and target balance.
*
* Requirements:
* - `from` cannot be the zero address
* - `from` must have sufficient balance
* - If caller is different from `from`, caller must have allowance for `from`'s tokens
*
* Emits:
* - {Burn} event with source address and amount
* - {Transfer} event from source address to zero address
*
* @param from Address to burn tokens from
* @param amount Amount of tokens to burn
*/
function burn(address from, uint256 amount) external {
_burn(from, _msgSender(), amount);
_burn(from, amount);
if (from != spender) _spendAllowance(from, spender, amount);
emit Burn(from, amount);
}

/**
Expand All @@ -125,16 +107,4 @@ contract ERC20Mintable is IERC20Mintable, Ownable2Step, ERC20Permit {
function renounceOwnership() public pure override {
revert RenounceOwnershipDisabled();
}

/**
* @dev Internal function to burn tokens, handling allowance checks if necessary.
* @param from Address to burn tokens from
* @param spender Address initiating the burn (for allowance checks if different from `from`)
* @param amount Amount of tokens to burn
*/
function _burn(address from, address spender, uint256 amount) internal {
_burn(from, amount);
if (from != spender) _spendAllowance(from, spender, amount);
emit Burn(from, amount);
}
}
22 changes: 22 additions & 0 deletions src/unit/GenericUnit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,28 @@ contract GenericUnit is IGenericShare, ERC20Mintable {
ERC20Mintable(controller, name, symbol)
{ }

/**
* @notice Burns tokens from the specified address, decreasing total supply.
* @dev Callable by any address. Decreases both total supply and target balance.
*
* Requirements:
* - `from` cannot be the zero address
* - `from` must have sufficient balance
* - If caller is different from `from`, caller must have allowance for `from`'s tokens
*
* Emits:
* - {Burn} event with source address and amount
* - {Transfer} event from source address to zero address
*
* @param from Address to burn tokens from
* @param amount Amount of tokens to burn
*/
function burn(address from, uint256 amount) external {
_burn(from, amount);
if (from != _msgSender()) _spendAllowance(from, _msgSender(), amount);
emit Burn(from, amount);
}

/**
* @notice Returns the address of the Vault for the given asset.
* @dev Vault changes do not emit VaultChange event as this is handled by the Controller.
Expand Down
75 changes: 1 addition & 74 deletions tests/unit/unit/ERC20Mintable.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ contract ERC20Mintable_Mint_Test is ERC20MintableTest {
}
}

contract ERC20Mintable_BurnOnlyOwner_Test is ERC20MintableTest {
contract ERC20Mintable_Burn_Test is ERC20MintableTest {
address from = makeAddr("from");
address spender = from;
uint256 initialBalance = 100 ether;
Expand Down Expand Up @@ -136,79 +136,6 @@ contract ERC20Mintable_BurnOnlyOwner_Test is ERC20MintableTest {
}
}

contract ERC20Mintable_Burn_Test is ERC20MintableTest {
address from = makeAddr("from");
uint256 initialBalance = 100 ether;

function setUp() public override {
super.setUp();

vm.prank(owner);
token.mint(from, initialBalance);
}

function testFuzz_shouldBurn(uint256 amount) public {
amount = bound(amount, 0, initialBalance);

vm.prank(from);
token.burn(from, amount);

assertEq(token.totalSupply(), initialBalance - amount);
assertEq(token.balanceOf(from), initialBalance - amount);
}

function test_shouldSpendAllowance() public {
uint256 amount = initialBalance / 2;
address spender = makeAddr("spender");

vm.prank(from);
token.approve(spender, amount);

vm.prank(spender);
token.burn(from, amount);

assertEq(token.totalSupply(), initialBalance - amount);
assertEq(token.balanceOf(from), initialBalance - amount);
assertEq(token.allowance(from, spender), 0);
}

function testFuzz_shouldEmit_Burn(uint256 amount) public {
amount = bound(amount, 0, initialBalance);

vm.expectEmit();
emit IERC20Mintable.Burn(from, amount);

vm.prank(from);
token.burn(from, amount);
}

function test_shouldRevert_whenFromZero() public {
vm.prank(from);
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidSender.selector, address(0)));
token.burn(address(0), 1);
}

function testFuzz_shouldRevert_whenInsufficientBalance(uint256 amount) public {
amount = bound(amount, initialBalance + 1, type(uint256).max);

vm.prank(from);
vm.expectRevert(
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, from, initialBalance, amount)
);
token.burn(from, amount);
}

function test_shouldRevert_whenSpenderNotApproved() public {
address spender = makeAddr("spender");

vm.expectRevert(
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, spender, 0, initialBalance)
);
vm.prank(spender);
token.burn(from, initialBalance);
}
}

contract ERC20Mintable_RenounceOwnership_Test is ERC20MintableTest {
function test_shouldRevert_whenRenounceOwnership_whenOwner() public {
vm.prank(owner);
Expand Down
75 changes: 75 additions & 0 deletions tests/unit/unit/GenericUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ pragma solidity 0.8.29;
import { Test } from "forge-std/Test.sol";

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";

import { GenericUnit, IController } from "../../../src/unit/GenericUnit.sol";
import { IERC20Mintable } from "../../../src/interfaces/IERC20Mintable.sol";

abstract contract GenericUnitTest is Test {
GenericUnit unit;
Expand Down Expand Up @@ -35,6 +37,79 @@ contract GenericUnit_Constructor_Test is GenericUnitTest {
}
}

contract GenericUnit_Burn_Test is GenericUnitTest {
address from = makeAddr("from");
uint256 initialBalance = 100 ether;

function setUp() public override {
super.setUp();

vm.prank(owner);
unit.mint(from, initialBalance);
}

function testFuzz_shouldBurn(uint256 amount) public {
amount = bound(amount, 0, initialBalance);

vm.prank(from);
unit.burn(from, amount);

assertEq(unit.totalSupply(), initialBalance - amount);
assertEq(unit.balanceOf(from), initialBalance - amount);
}

function test_shouldSpendAllowance() public {
uint256 amount = initialBalance / 2;
address spender = makeAddr("spender");

vm.prank(from);
unit.approve(spender, amount);

vm.prank(spender);
unit.burn(from, amount);

assertEq(unit.totalSupply(), initialBalance - amount);
assertEq(unit.balanceOf(from), initialBalance - amount);
assertEq(unit.allowance(from, spender), 0);
}

function testFuzz_shouldEmit_Burn(uint256 amount) public {
amount = bound(amount, 0, initialBalance);

vm.expectEmit();
emit IERC20Mintable.Burn(from, amount);

vm.prank(from);
unit.burn(from, amount);
}

function test_shouldRevert_whenFromZero() public {
vm.prank(from);
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidSender.selector, address(0)));
unit.burn(address(0), 1);
}

function testFuzz_shouldRevert_whenInsufficientBalance(uint256 amount) public {
amount = bound(amount, initialBalance + 1, type(uint256).max);

vm.prank(from);
vm.expectRevert(
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, from, initialBalance, amount)
);
unit.burn(from, amount);
}

function test_shouldRevert_whenSpenderNotApproved() public {
address spender = makeAddr("spender");

vm.expectRevert(
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, spender, 0, initialBalance)
);
vm.prank(spender);
unit.burn(from, initialBalance);
}
}

contract GenericUnit_Vault_Test is GenericUnitTest {
function setUp() public override {
super.setUp();
Expand Down