From 0102136aec60665873df56e444d1e44183f6843e Mon Sep 17 00:00:00 2001 From: vince-grondin Date: Tue, 12 Dec 2023 06:47:50 -1000 Subject: [PATCH 1/3] Add a solution for the Minimal Tokens Exercise Closes #34 --- src/UnburnableToken.sol | 98 ++++++++++ test/UnburnableToken.t.sol | 363 +++++++++++++++++++++++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 src/UnburnableToken.sol create mode 100644 test/UnburnableToken.t.sol diff --git a/src/UnburnableToken.sol b/src/UnburnableToken.sol new file mode 100644 index 0000000..b55daa8 --- /dev/null +++ b/src/UnburnableToken.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +/** + * @title Solution for the [Minimal Tokens Exercise](https://docs.base.org/base-camp/docs/minimal-tokens/minimal-tokens-exercise). + * @author Roch + */ +contract UnburnableToken { + mapping(address => uint) public balances; + + uint16 immutable MAX_CLAIMABLE = 1000; + + uint32 immutable MAX_SUPPLY = 100000000; + + uint public totalSupply; + + uint public totalClaimed; + + error AllTokensClaimed(); + + error LowTransferAmount(uint amount); + + error InsufficientSupply(uint remainingSupply); + + error ExceedsUserMaxClaimable(uint maxClaimable); + + error InsufficientSenderSupply(uint balance); + + error UnsafeTransfer(address sender); + + error TokensClaimed(uint claimed); + + event TokensClaimedEvent( + address recipient, + uint amount, + uint balance, + uint totalSupply, + uint totalClaimed + ); + + constructor(uint32 _totalSupply) { + totalSupply = _totalSupply > 0 ? _totalSupply : MAX_SUPPLY; + } + + /** + * @notice Adds the supplied amount to the sender's balance. + * @dev Reverts with a `TokensClaimed` error if a sender tries to claim more than once. + * Reverts with a `AllTokensClaimed` error if there are no tokens left to claim. + * Reverts with a `InsufficientSupply` error if there are not enough tokens left to claim. + * @param _amount The amount to add to the sender's balance. + * @return Whether the operation succeeded. + */ + function claim(uint _amount) public returns (bool) { + if (balances[msg.sender] != 0) + revert TokensClaimed(balances[msg.sender]); + + if (totalSupply == 0) + revert AllTokensClaimed(); + + if (_amount > totalSupply) + revert InsufficientSupply(totalSupply); + + if (_amount > MAX_CLAIMABLE) + revert ExceedsUserMaxClaimable(MAX_CLAIMABLE); + + balances[msg.sender] = _amount; + totalSupply -= _amount; + totalClaimed += _amount; + + emit TokensClaimedEvent( + msg.sender, + _amount, + balances[msg.sender], + totalSupply, + totalClaimed + ); + + return true; + } + + /** + * @notice Transfer the `_amount` from the sender to the `_to` address. + * @param _to The address where to transfer the `_amount` amount. + * @param _amount The amount to transfer. + */ + function safeTransfer(address _to, uint _amount) public returns (bool) { + if (_to == address(0) || _amount <= 0) + revert UnsafeTransfer(_to); + + if (balances[msg.sender] < _amount) + revert InsufficientSenderSupply(balances[msg.sender]); + + balances[_to] += _amount; + balances[msg.sender] -= _amount; + + return true; + } +} diff --git a/test/UnburnableToken.t.sol b/test/UnburnableToken.t.sol new file mode 100644 index 0000000..eab5ec4 --- /dev/null +++ b/test/UnburnableToken.t.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import "forge-std/Test.sol"; +import "../src/UnburnableToken.sol"; + +/** + * @title Verifies the behavior of the `UnburnableToken` contract. + * @author Roch + */ +contract UnburnableTokenTest is Test { + using AddressGenerator for uint; + + uint32 immutable MAX_CLAIMABLE = 1000; + uint32 immutable MAX_SUPPLY = 10000; + + UnburnableToken private unburnableToken; + + address private zeroAddress = address(0); + address private userA = address(1); + address private userB = address(2); + address private userC = address(3); + + event TokensClaimedEvent( + address recipient, + uint amount, + uint balance, + uint totalSupply, + uint totalClaimed + ); + + function setUp() public { + unburnableToken = new UnburnableToken(MAX_SUPPLY); + } + + /** + * @dev Verifies that calling `claim` with an amount greater than the remaining supply reverts with an + * `InsufficientSupply` error. + * @param _address1 An address greater than 200000. + * @param _address2 Another address greater than 200000. + */ + function test_GivenClaimedAmountGreaterThanRemainingSupply_WhenClaiming_ThenInsufficientSupplyRevert( + uint _address1, + uint _address2 + ) public { + vm.assume(_address1 > 10000); + vm.assume(_address2 > 10000); + vm.assume(_address1 != _address2); + + uint iterationAmount = 500; + uint claimIterations = (MAX_SUPPLY / 500) - 1; + claimAmountForDistinctUsers(iterationAmount, claimIterations); + + vm.startPrank(_address1.toAddress()); + unburnableToken.claim(300); + vm.stopPrank(); + + vm.startPrank(_address2.toAddress()); + expectInsufficientSupplyRevert(200); + unburnableToken.claim(201); + + assertEq(unburnableToken.totalSupply(), 200); + assertEq(unburnableToken.totalClaimed(), MAX_SUPPLY - 200); + + for (uint i = 1; i <= claimIterations; i++) { + assertEq(unburnableToken.balances(i.toAddress()), iterationAmount); + } + + assertEq(unburnableToken.balances(_address2.toAddress()), 0); + } + + /** + * @dev Verifies that calling `claim` once all tokens were claimed reverts with a `AllTokensClaimed` error. + * @param _amount1 The amount to claimed after all tokens were claimed. + * @param _address The address of the user claiming the tokens after they were all already claimed. + */ + function test_GivenTotalSupplyDistributed_WhenClaiming_ThenAllTokensClaimedRevert( + uint _amount1, + uint _address + ) public { + vm.assume(_address > 10000); + uint iterationAmount = 500; + uint claimIterations = MAX_SUPPLY / 500; + claimAmountForDistinctUsers(iterationAmount, claimIterations); + + vm.startPrank(_address.toAddress()); + expectAllTokensClaimedRevert(); + unburnableToken.claim(_amount1); + + assertEq(unburnableToken.totalSupply(), 0); + assertEq(unburnableToken.totalClaimed(), MAX_SUPPLY); + + for (uint i = 1; i <= claimIterations; i++) { + assertEq(unburnableToken.balances(i.toAddress()), iterationAmount); + } + + assertEq(unburnableToken.balances(_address.toAddress()), 0); + } + + /** + * @dev Verifies that calling `claim` reverts with an `ExceedsUserMaxClaimable` error when the sender claims more + * than the maximum amount allowed to be claimed by a user. + * @param _amount The amount to claim by the user, greater than the maximum claimable amount. + */ + function test_GivenAmountGreaterThanOneThousand_WhenClaiming_ThenTokensClaimedRevert( + uint _amount + ) public { + vm.assume(_amount > MAX_CLAIMABLE && _amount < MAX_SUPPLY); + + vm.startPrank(userA); + expectExceedsUserMaxClaimableRevert(); + unburnableToken.claim(_amount); + + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY); + assertEq(unburnableToken.totalClaimed(), 0); + assertEq(unburnableToken.balances(userA), 0); + } + + /** + * @dev Verifies that calling `claim` reverts with a `TokensClaimed` when the sender has already claimed one or more + * tokens. + * @param _amount The amount claimed by the user. + */ + function test_GivenTokensAlreadyClaimedBySender_WhenClaiming_ThenTokensClaimedRevert( + uint _amount + ) public { + vm.assume(_amount > 0 && _amount < MAX_CLAIMABLE); + + vm.startPrank(userA); + unburnableToken.claim(_amount); + + expectTokensClaimedRevert(_amount); + + unburnableToken.claim(1); + + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - _amount); + assertEq(unburnableToken.totalClaimed(), _amount); + assertEq(unburnableToken.balances(userA), _amount); + } + + /** + * @dev Verifies that calling `claim` when there's enough supply left and no tokens were previously claimed by the + * sender succeeds. + * @param _amount The amount claimed by a user. + * @param _address The address of the user claiming the tokens after they were all already claimed. + */ + function test_GivenEnoughSupplyLeft_AndNoTokensClaimedBySender_WhenClaiming_ThenSuccess( + uint _amount, + uint _address + ) public { + uint iterationAmount = 500; + vm.assume(_amount > 0 && _amount < iterationAmount); + vm.assume(_address > 10000); + + uint claimIterations = (MAX_SUPPLY / 500) - 1; + claimAmountForDistinctUsers(iterationAmount, claimIterations); + + vm.startPrank(_address.toAddress()); + unburnableToken.claim(_amount); + + uint expectedTotalClaimedAmount = (iterationAmount * claimIterations) + + _amount; + assertEq( + unburnableToken.totalSupply(), + MAX_SUPPLY - expectedTotalClaimedAmount + ); + assertEq(unburnableToken.totalClaimed(), expectedTotalClaimedAmount); + + for (uint i = 1; i <= claimIterations; i++) { + assertEq(unburnableToken.balances(i.toAddress()), iterationAmount); + } + + assertEq(unburnableToken.balances(_address.toAddress()), _amount); + } + + /** + * @dev Verifies that calling `safeTransfer` with a `_to` zero address reverts with an `UnsafeTransfer` error. + * @param _amount The amount to transfer. + */ + function test_GivenToAddressIsZeroAddress_WhenSafeTransfer_ThenUnsafeTransferRevert( + uint _amount + ) public { + vm.assume(_amount > 0); + + expectUnsafeTransferRevert(zeroAddress); + + unburnableToken.safeTransfer(zeroAddress, _amount); + + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY); + assertEq(unburnableToken.totalClaimed(), 0); + assertEq(unburnableToken.balances(userA), 0); + assertEq(unburnableToken.balances(zeroAddress), 0); + } + + /** + * @dev Verifies that calling `safeTransfer` with a `_amount` zero reverts with an `UnsafeTransfer` error. + * @param _address The address of the sender. + */ + function test_GivenAmountIsZero_WhenSafeTransfer_ThenUnsafeTransferRevert( + address _address + ) public { + vm.assume(_address != zeroAddress); + uint amount = 0; + + vm.startPrank(userA); + expectUnsafeTransferRevert(_address); + unburnableToken.safeTransfer(_address, amount); + + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY); + assertEq(unburnableToken.totalClaimed(), 0); + assertEq(unburnableToken.balances(userA), 0); + assertEq(unburnableToken.balances(zeroAddress), 0); + } + + /** + * @dev Verifies that calling `safeTransfer` to transfer an amount greater than the sender's balance reverts with a + * `InsufficientSenderSupply` error. + * @param _sender The address of the sender. + * @param _to The address to transfer the amount to. + * @param _senderBalance The balance of the sender. + * @param _transferAmount The amount to transfer from the sender to the `_to` recipient. + */ + function test_GivenAmountGreaterThanSenderBalance_WhenSafeTransfer_ThenInsufficientSenderSupplyRevert( + address _sender, + address _to, + uint _senderBalance, + uint _transferAmount + ) public { + vm.assume(_sender != zeroAddress); + vm.assume(_to != zeroAddress); + vm.assume(_senderBalance > 0 && _senderBalance <= MAX_CLAIMABLE); + vm.assume(_transferAmount > _senderBalance); + + vm.startPrank(_sender); + unburnableToken.claim(_senderBalance); + + expectInsufficientSenderSupplyRevert(_senderBalance); + + unburnableToken.safeTransfer(_to, _transferAmount); + + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - _senderBalance); + assertEq(unburnableToken.totalClaimed(), _senderBalance); + assertEq(unburnableToken.balances(_sender), _senderBalance); + assertEq(unburnableToken.balances(_to), 0); + } + + function test_GivenNonZeroAddress_AndAmountLowerOrEqualSenderBalance_WehnSafeTransfer_ThenSuccess( + address _sender, + address _to, + uint _senderBalance, + uint _transferAmount + ) public { + vm.assume(_sender != zeroAddress); + vm.assume(_to != zeroAddress); + vm.assume(_senderBalance > 0 && _senderBalance <= MAX_CLAIMABLE); + vm.assume(_transferAmount > 0 && _transferAmount <= _senderBalance); + + vm.startPrank(_sender); + unburnableToken.claim(_senderBalance); + + bool result = unburnableToken.safeTransfer(_to, _transferAmount); + + assertTrue(result); + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - _senderBalance); + assertEq(unburnableToken.totalClaimed(), _senderBalance); + assertEq( + unburnableToken.balances(_sender), + _senderBalance - _transferAmount + ); + assertEq(unburnableToken.balances(_to), _transferAmount); + } + + /** + * @dev Helper function to claim `_amount` for `_iterations` distinct addresses. + * @param _amount The amount to claim for each generated address. + * @param iterations The number of generated address to attribute the `_amount` to. + */ + function claimAmountForDistinctUsers( + uint _amount, + uint iterations + ) private { + for (uint i = 1; i <= iterations; i++) { + vm.startPrank(i.toAddress()); + unburnableToken.claim(_amount); + vm.stopPrank(); + } + } + + /** + * @dev Helper function to verify that an `AllTokensClaimed` revert occurs. + */ + function expectAllTokensClaimedRevert() private { + vm.expectRevert( + abi.encodeWithSelector(UnburnableToken.AllTokensClaimed.selector) + ); + } + + /** + * @dev Helper function to verify that an `ExceedsUserMaxClaimable` revert occurs. + */ + function expectExceedsUserMaxClaimableRevert() private { + vm.expectRevert( + abi.encodeWithSelector( + UnburnableToken.ExceedsUserMaxClaimable.selector, + MAX_CLAIMABLE + ) + ); + } + + /** + * @dev Helper function to verify that an `InsufficientSupply` revert occurs. + */ + function expectInsufficientSupplyRevert(uint remainingSupply) private { + vm.expectRevert( + abi.encodeWithSelector( + UnburnableToken.InsufficientSupply.selector, + remainingSupply + ) + ); + } + + /** + * @dev Helper function to verify that an `InsufficientSenderSupply` revert occurs. + */ + function expectInsufficientSenderSupplyRevert(uint _balance) private { + vm.expectRevert( + abi.encodeWithSelector( + UnburnableToken.InsufficientSenderSupply.selector, + _balance + ) + ); + } + + /** + * @dev Helper function to verify that a `TokensClaimed` revert occurs. + */ + function expectTokensClaimedRevert(uint claimed) private { + vm.expectRevert( + abi.encodeWithSelector( + UnburnableToken.TokensClaimed.selector, + claimed + ) + ); + } + + /** + * @dev Helper function to verify that an `UnsafeTransfer` revert occurs. + */ + function expectUnsafeTransferRevert(address _address) private { + vm.expectRevert( + abi.encodeWithSelector( + UnburnableToken.UnsafeTransfer.selector, + _address + ) + ); + } +} + +library AddressGenerator { + function toAddress(uint _address) public pure returns (address) { + return address(uint160(uint256(keccak256(abi.encodePacked(_address))))); + } +} From 13c151c0051b16202619e400ca97e24be1a8a591 Mon Sep 17 00:00:00 2001 From: vince-grondin Date: Tue, 12 Dec 2023 07:29:57 -1000 Subject: [PATCH 2/3] Modify solution to Minimal Tokens Exercise to not take _amount arg. in claim method Refs #34 --- src/UnburnableToken.sol | 21 ++---- test/UnburnableToken.t.sol | 127 ++++++++----------------------------- 2 files changed, 33 insertions(+), 115 deletions(-) diff --git a/src/UnburnableToken.sol b/src/UnburnableToken.sol index b55daa8..2af3cc0 100644 --- a/src/UnburnableToken.sol +++ b/src/UnburnableToken.sol @@ -28,7 +28,7 @@ contract UnburnableToken { error UnsafeTransfer(address sender); - error TokensClaimed(uint claimed); + error TokensClaimed(uint balance); event TokensClaimedEvent( address recipient, @@ -43,33 +43,26 @@ contract UnburnableToken { } /** - * @notice Adds the supplied amount to the sender's balance. + * @notice Adds the maximum claimable amount per user to the sender's balance. * @dev Reverts with a `TokensClaimed` error if a sender tries to claim more than once. * Reverts with a `AllTokensClaimed` error if there are no tokens left to claim. * Reverts with a `InsufficientSupply` error if there are not enough tokens left to claim. - * @param _amount The amount to add to the sender's balance. * @return Whether the operation succeeded. */ - function claim(uint _amount) public returns (bool) { + function claim() public returns (bool) { if (balances[msg.sender] != 0) revert TokensClaimed(balances[msg.sender]); if (totalSupply == 0) revert AllTokensClaimed(); - if (_amount > totalSupply) - revert InsufficientSupply(totalSupply); - - if (_amount > MAX_CLAIMABLE) - revert ExceedsUserMaxClaimable(MAX_CLAIMABLE); - - balances[msg.sender] = _amount; - totalSupply -= _amount; - totalClaimed += _amount; + balances[msg.sender] = MAX_CLAIMABLE; + totalSupply -= MAX_CLAIMABLE; + totalClaimed += MAX_CLAIMABLE; emit TokensClaimedEvent( msg.sender, - _amount, + MAX_CLAIMABLE, balances[msg.sender], totalSupply, totalClaimed diff --git a/test/UnburnableToken.t.sol b/test/UnburnableToken.t.sol index eab5ec4..bff1484 100644 --- a/test/UnburnableToken.t.sol +++ b/test/UnburnableToken.t.sol @@ -33,133 +33,65 @@ contract UnburnableTokenTest is Test { unburnableToken = new UnburnableToken(MAX_SUPPLY); } - /** - * @dev Verifies that calling `claim` with an amount greater than the remaining supply reverts with an - * `InsufficientSupply` error. - * @param _address1 An address greater than 200000. - * @param _address2 Another address greater than 200000. - */ - function test_GivenClaimedAmountGreaterThanRemainingSupply_WhenClaiming_ThenInsufficientSupplyRevert( - uint _address1, - uint _address2 - ) public { - vm.assume(_address1 > 10000); - vm.assume(_address2 > 10000); - vm.assume(_address1 != _address2); - - uint iterationAmount = 500; - uint claimIterations = (MAX_SUPPLY / 500) - 1; - claimAmountForDistinctUsers(iterationAmount, claimIterations); - - vm.startPrank(_address1.toAddress()); - unburnableToken.claim(300); - vm.stopPrank(); - - vm.startPrank(_address2.toAddress()); - expectInsufficientSupplyRevert(200); - unburnableToken.claim(201); - - assertEq(unburnableToken.totalSupply(), 200); - assertEq(unburnableToken.totalClaimed(), MAX_SUPPLY - 200); - - for (uint i = 1; i <= claimIterations; i++) { - assertEq(unburnableToken.balances(i.toAddress()), iterationAmount); - } - - assertEq(unburnableToken.balances(_address2.toAddress()), 0); - } - /** * @dev Verifies that calling `claim` once all tokens were claimed reverts with a `AllTokensClaimed` error. - * @param _amount1 The amount to claimed after all tokens were claimed. * @param _address The address of the user claiming the tokens after they were all already claimed. */ function test_GivenTotalSupplyDistributed_WhenClaiming_ThenAllTokensClaimedRevert( - uint _amount1, uint _address ) public { vm.assume(_address > 10000); - uint iterationAmount = 500; - uint claimIterations = MAX_SUPPLY / 500; - claimAmountForDistinctUsers(iterationAmount, claimIterations); + uint claimIterations = MAX_SUPPLY / MAX_CLAIMABLE; + claimAmountForDistinctUsers(claimIterations); vm.startPrank(_address.toAddress()); expectAllTokensClaimedRevert(); - unburnableToken.claim(_amount1); + unburnableToken.claim(); assertEq(unburnableToken.totalSupply(), 0); assertEq(unburnableToken.totalClaimed(), MAX_SUPPLY); for (uint i = 1; i <= claimIterations; i++) { - assertEq(unburnableToken.balances(i.toAddress()), iterationAmount); + assertEq(unburnableToken.balances(i.toAddress()), MAX_CLAIMABLE); } assertEq(unburnableToken.balances(_address.toAddress()), 0); } - /** - * @dev Verifies that calling `claim` reverts with an `ExceedsUserMaxClaimable` error when the sender claims more - * than the maximum amount allowed to be claimed by a user. - * @param _amount The amount to claim by the user, greater than the maximum claimable amount. - */ - function test_GivenAmountGreaterThanOneThousand_WhenClaiming_ThenTokensClaimedRevert( - uint _amount - ) public { - vm.assume(_amount > MAX_CLAIMABLE && _amount < MAX_SUPPLY); - - vm.startPrank(userA); - expectExceedsUserMaxClaimableRevert(); - unburnableToken.claim(_amount); - - assertEq(unburnableToken.totalSupply(), MAX_SUPPLY); - assertEq(unburnableToken.totalClaimed(), 0); - assertEq(unburnableToken.balances(userA), 0); - } - /** * @dev Verifies that calling `claim` reverts with a `TokensClaimed` when the sender has already claimed one or more * tokens. - * @param _amount The amount claimed by the user. */ - function test_GivenTokensAlreadyClaimedBySender_WhenClaiming_ThenTokensClaimedRevert( - uint _amount - ) public { - vm.assume(_amount > 0 && _amount < MAX_CLAIMABLE); - + function test_GivenTokensAlreadyClaimedBySender_WhenClaiming_ThenTokensClaimedRevert() public { vm.startPrank(userA); - unburnableToken.claim(_amount); + unburnableToken.claim(); - expectTokensClaimedRevert(_amount); + expectTokensClaimedRevert(MAX_CLAIMABLE); - unburnableToken.claim(1); + unburnableToken.claim(); - assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - _amount); - assertEq(unburnableToken.totalClaimed(), _amount); - assertEq(unburnableToken.balances(userA), _amount); + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - MAX_CLAIMABLE); + assertEq(unburnableToken.totalClaimed(), MAX_CLAIMABLE); + assertEq(unburnableToken.balances(userA), MAX_CLAIMABLE); } /** * @dev Verifies that calling `claim` when there's enough supply left and no tokens were previously claimed by the * sender succeeds. - * @param _amount The amount claimed by a user. * @param _address The address of the user claiming the tokens after they were all already claimed. */ function test_GivenEnoughSupplyLeft_AndNoTokensClaimedBySender_WhenClaiming_ThenSuccess( - uint _amount, uint _address ) public { - uint iterationAmount = 500; - vm.assume(_amount > 0 && _amount < iterationAmount); vm.assume(_address > 10000); - uint claimIterations = (MAX_SUPPLY / 500) - 1; - claimAmountForDistinctUsers(iterationAmount, claimIterations); + uint claimIterations = (MAX_SUPPLY / MAX_CLAIMABLE) - 1; + claimAmountForDistinctUsers(claimIterations); vm.startPrank(_address.toAddress()); - unburnableToken.claim(_amount); + unburnableToken.claim(); - uint expectedTotalClaimedAmount = (iterationAmount * claimIterations) + - _amount; + uint expectedTotalClaimedAmount = (MAX_CLAIMABLE * claimIterations) + MAX_CLAIMABLE; assertEq( unburnableToken.totalSupply(), MAX_SUPPLY - expectedTotalClaimedAmount @@ -167,10 +99,10 @@ contract UnburnableTokenTest is Test { assertEq(unburnableToken.totalClaimed(), expectedTotalClaimedAmount); for (uint i = 1; i <= claimIterations; i++) { - assertEq(unburnableToken.balances(i.toAddress()), iterationAmount); + assertEq(unburnableToken.balances(i.toAddress()), MAX_CLAIMABLE); } - assertEq(unburnableToken.balances(_address.toAddress()), _amount); + assertEq(unburnableToken.balances(_address.toAddress()), MAX_CLAIMABLE); } /** @@ -217,22 +149,20 @@ contract UnburnableTokenTest is Test { * `InsufficientSenderSupply` error. * @param _sender The address of the sender. * @param _to The address to transfer the amount to. - * @param _senderBalance The balance of the sender. * @param _transferAmount The amount to transfer from the sender to the `_to` recipient. */ function test_GivenAmountGreaterThanSenderBalance_WhenSafeTransfer_ThenInsufficientSenderSupplyRevert( address _sender, address _to, - uint _senderBalance, uint _transferAmount ) public { + uint _senderBalance = MAX_CLAIMABLE; vm.assume(_sender != zeroAddress); vm.assume(_to != zeroAddress); - vm.assume(_senderBalance > 0 && _senderBalance <= MAX_CLAIMABLE); vm.assume(_transferAmount > _senderBalance); vm.startPrank(_sender); - unburnableToken.claim(_senderBalance); + unburnableToken.claim(); expectInsufficientSenderSupplyRevert(_senderBalance); @@ -247,16 +177,15 @@ contract UnburnableTokenTest is Test { function test_GivenNonZeroAddress_AndAmountLowerOrEqualSenderBalance_WehnSafeTransfer_ThenSuccess( address _sender, address _to, - uint _senderBalance, uint _transferAmount ) public { + uint _senderBalance = MAX_CLAIMABLE; vm.assume(_sender != zeroAddress); vm.assume(_to != zeroAddress); - vm.assume(_senderBalance > 0 && _senderBalance <= MAX_CLAIMABLE); vm.assume(_transferAmount > 0 && _transferAmount <= _senderBalance); vm.startPrank(_sender); - unburnableToken.claim(_senderBalance); + unburnableToken.claim(); bool result = unburnableToken.safeTransfer(_to, _transferAmount); @@ -271,17 +200,13 @@ contract UnburnableTokenTest is Test { } /** - * @dev Helper function to claim `_amount` for `_iterations` distinct addresses. - * @param _amount The amount to claim for each generated address. + * @dev Helper function to claim for `_iterations` distinct addresses. * @param iterations The number of generated address to attribute the `_amount` to. */ - function claimAmountForDistinctUsers( - uint _amount, - uint iterations - ) private { + function claimAmountForDistinctUsers(uint iterations) private { for (uint i = 1; i <= iterations; i++) { vm.startPrank(i.toAddress()); - unburnableToken.claim(_amount); + unburnableToken.claim(); vm.stopPrank(); } } @@ -334,11 +259,11 @@ contract UnburnableTokenTest is Test { /** * @dev Helper function to verify that a `TokensClaimed` revert occurs. */ - function expectTokensClaimedRevert(uint claimed) private { + function expectTokensClaimedRevert(uint _balance) private { vm.expectRevert( abi.encodeWithSelector( UnburnableToken.TokensClaimed.selector, - claimed + _balance ) ); } From 5eb735ffa1a5dfbe56d5da0397d213426f8772c8 Mon Sep 17 00:00:00 2001 From: vince-grondin Date: Tue, 12 Dec 2023 07:31:05 -1000 Subject: [PATCH 3/3] Modify solution to Minimal Tokens Exercise to not populate TokensClaimed error, not have return types, check _to balance instead of internal balance, use hardcoded 0 address instead of address(0) Refs #34 --- src/UnburnableToken.sol | 15 +++------ test/UnburnableToken.t.sol | 64 ++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/UnburnableToken.sol b/src/UnburnableToken.sol index 2af3cc0..3f49256 100644 --- a/src/UnburnableToken.sol +++ b/src/UnburnableToken.sol @@ -28,7 +28,7 @@ contract UnburnableToken { error UnsafeTransfer(address sender); - error TokensClaimed(uint balance); + error TokensClaimed(); event TokensClaimedEvent( address recipient, @@ -47,11 +47,10 @@ contract UnburnableToken { * @dev Reverts with a `TokensClaimed` error if a sender tries to claim more than once. * Reverts with a `AllTokensClaimed` error if there are no tokens left to claim. * Reverts with a `InsufficientSupply` error if there are not enough tokens left to claim. - * @return Whether the operation succeeded. */ - function claim() public returns (bool) { + function claim() public { if (balances[msg.sender] != 0) - revert TokensClaimed(balances[msg.sender]); + revert TokensClaimed(); if (totalSupply == 0) revert AllTokensClaimed(); @@ -67,8 +66,6 @@ contract UnburnableToken { totalSupply, totalClaimed ); - - return true; } /** @@ -76,8 +73,8 @@ contract UnburnableToken { * @param _to The address where to transfer the `_amount` amount. * @param _amount The amount to transfer. */ - function safeTransfer(address _to, uint _amount) public returns (bool) { - if (_to == address(0) || _amount <= 0) + function safeTransfer(address _to, uint _amount) public { + if (_to == 0x0000000000000000000000000000000000000000 || _to.balance == 0) revert UnsafeTransfer(_to); if (balances[msg.sender] < _amount) @@ -85,7 +82,5 @@ contract UnburnableToken { balances[_to] += _amount; balances[msg.sender] -= _amount; - - return true; } } diff --git a/test/UnburnableToken.t.sol b/test/UnburnableToken.t.sol index bff1484..dc8fe6b 100644 --- a/test/UnburnableToken.t.sol +++ b/test/UnburnableToken.t.sol @@ -62,11 +62,13 @@ contract UnburnableTokenTest is Test { * @dev Verifies that calling `claim` reverts with a `TokensClaimed` when the sender has already claimed one or more * tokens. */ - function test_GivenTokensAlreadyClaimedBySender_WhenClaiming_ThenTokensClaimedRevert() public { + function test_GivenTokensAlreadyClaimedBySender_WhenClaiming_ThenTokensClaimedRevert() + public + { vm.startPrank(userA); unburnableToken.claim(); - expectTokensClaimedRevert(MAX_CLAIMABLE); + expectTokensClaimedRevert(); unburnableToken.claim(); @@ -91,7 +93,8 @@ contract UnburnableTokenTest is Test { vm.startPrank(_address.toAddress()); unburnableToken.claim(); - uint expectedTotalClaimedAmount = (MAX_CLAIMABLE * claimIterations) + MAX_CLAIMABLE; + uint expectedTotalClaimedAmount = (MAX_CLAIMABLE * claimIterations) + + MAX_CLAIMABLE; assertEq( unburnableToken.totalSupply(), MAX_SUPPLY - expectedTotalClaimedAmount @@ -112,7 +115,7 @@ contract UnburnableTokenTest is Test { function test_GivenToAddressIsZeroAddress_WhenSafeTransfer_ThenUnsafeTransferRevert( uint _amount ) public { - vm.assume(_amount > 0); + vm.assume(_amount > 0 && _amount <= MAX_CLAIMABLE); expectUnsafeTransferRevert(zeroAddress); @@ -125,18 +128,23 @@ contract UnburnableTokenTest is Test { } /** - * @dev Verifies that calling `safeTransfer` with a `_amount` zero reverts with an `UnsafeTransfer` error. - * @param _address The address of the sender. + * @dev Verifies that calling `safeTransfer` with a `_to` recipient that has a 0 balance reverts with an + * `UnsafeTransfer` error. + * @param _amount The amount to transfer. + * @param _to The address of the sender. */ - function test_GivenAmountIsZero_WhenSafeTransfer_ThenUnsafeTransferRevert( - address _address + function test_GivenRecipientNotFunded_WhenSafeTransfer_ThenUnsafeTransferRevert( + uint _amount, + address _to ) public { - vm.assume(_address != zeroAddress); - uint amount = 0; + vm.assume(_amount > 0 && _amount <= MAX_CLAIMABLE); + vm.assume(_to != zeroAddress && _to != userA); + + vm.deal(_to, 0 ether); vm.startPrank(userA); - expectUnsafeTransferRevert(_address); - unburnableToken.safeTransfer(_address, amount); + expectUnsafeTransferRevert(_to); + unburnableToken.safeTransfer(_to, _amount); assertEq(unburnableToken.totalSupply(), MAX_SUPPLY); assertEq(unburnableToken.totalClaimed(), 0); @@ -158,9 +166,11 @@ contract UnburnableTokenTest is Test { ) public { uint _senderBalance = MAX_CLAIMABLE; vm.assume(_sender != zeroAddress); - vm.assume(_to != zeroAddress); + vm.assume(_to != _sender); vm.assume(_transferAmount > _senderBalance); + vm.deal(_to, 1 ether); + vm.startPrank(_sender); unburnableToken.claim(); @@ -168,12 +178,19 @@ contract UnburnableTokenTest is Test { unburnableToken.safeTransfer(_to, _transferAmount); - assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - _senderBalance); - assertEq(unburnableToken.totalClaimed(), _senderBalance); + uint expectedClaimed = MAX_CLAIMABLE; + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - expectedClaimed); + assertEq(unburnableToken.totalClaimed(), expectedClaimed); assertEq(unburnableToken.balances(_sender), _senderBalance); assertEq(unburnableToken.balances(_to), 0); } + /** + * @dev Verifies that calling `safeTransfer` to transfer an amount lower or equal to the sender's balance succeeds. + * @param _sender The address of the sender. + * @param _to The address to transfer the amount to. + * @param _transferAmount The amount to transfer from the sender to the `_to` recipient. + */ function test_GivenNonZeroAddress_AndAmountLowerOrEqualSenderBalance_WehnSafeTransfer_ThenSuccess( address _sender, address _to, @@ -184,14 +201,16 @@ contract UnburnableTokenTest is Test { vm.assume(_to != zeroAddress); vm.assume(_transferAmount > 0 && _transferAmount <= _senderBalance); + vm.deal(_to, 1 ether); + vm.startPrank(_sender); unburnableToken.claim(); - bool result = unburnableToken.safeTransfer(_to, _transferAmount); + unburnableToken.safeTransfer(_to, _transferAmount); - assertTrue(result); - assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - _senderBalance); - assertEq(unburnableToken.totalClaimed(), _senderBalance); + uint expectedClaimed = MAX_CLAIMABLE; + assertEq(unburnableToken.totalSupply(), MAX_SUPPLY - expectedClaimed); + assertEq(unburnableToken.totalClaimed(), expectedClaimed); assertEq( unburnableToken.balances(_sender), _senderBalance - _transferAmount @@ -259,12 +278,9 @@ contract UnburnableTokenTest is Test { /** * @dev Helper function to verify that a `TokensClaimed` revert occurs. */ - function expectTokensClaimedRevert(uint _balance) private { + function expectTokensClaimedRevert() private { vm.expectRevert( - abi.encodeWithSelector( - UnburnableToken.TokensClaimed.selector, - _balance - ) + abi.encodeWithSelector(UnburnableToken.TokensClaimed.selector) ); }