Summary
No price to trade
Vulnerability Details
The vulnerability is in the SantasList::buyPresent
function in the SantasList.sol
contract starting at line 172.
The function should exchange 2e18 SantasTokens
for NFT
. In this function the exchange is for 1e18 tokens.
function buyPresent(address presentReceiver) external {
i_santaToken.burn(presentReceiver);
_mintAndIncrement();
}
Impact
The logic of this contract is violated. If you have 2e18 tokens, you can exchange them for 2 NFT.
Tools Used
forge
Recommendations
Working Test Case
The following test exchanges 1e18 tokens for 1 NFT
. When executed, this test will pass, demonstrating that the user can exchange 1e18 tokens for 1 NFT:
function testBuyPresent() public {
vm.startPrank(address(santasList));
santaToken.mint(user);
vm.stopPrank();
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
vm.startPrank(user);
assertEq(santaToken.balanceOf(user), 1e18);
santasList.buyPresent(user);
assertEq(santaToken.balanceOf(user), 0);
assertEq(santasList.balanceOf(user), 1);
vm.stopPrank();
}
Run the test:
forge test --mt testBuyPresent -vvvv
Which yields the following output:
Running 1 test for test/unit/SantasListTest.t.sol:SantasListTest
[PASS] testBuyPresent() (gas: 106807)
Traces:
[111020] SantasListTest::testBuyPresent()
├─ [0] VM::startPrank(SantasList: [0xE6E33783D6533ad50d373DDaa6fD21b272a57B69])
│ └─ ← ()
├─ [46713] SantaToken::mint(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D], amount: 1000000000000000000 [1e18])
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [283] SantasList::CHRISTMAS_2023_BLOCK_TIME() [staticcall]
│ └─ ← 1703480381 [1.703e9]
├─ [0] VM::warp(1703480382 [1.703e9])
│ └─ ← ()
├─ [0] VM::startPrank(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D])
│ └─ ← ()
├─ [542] SantaToken::balanceOf(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D]) [staticcall]
│ └─ ← 1000000000000000000 [1e18]
├─ [58439] SantasList::buyPresent(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D])
│ ├─ [2348] SantaToken::burn(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D])
│ │ ├─ emit Transfer(from: user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D], to: 0x0000000000000000000000000000000000000000, amount: 1000000000000000000 [1e18])
│ │ └─ ← ()
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D], tokenId: 0)
│ └─ ← ()
├─ [542] SantaToken::balanceOf(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D]) [staticcall]
│ └─ ← 0
├─ [678] SantasList::balanceOf(user: [0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D]) [staticcall]
│ └─ ← 1
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.84ms
Recommended Mitigation
We should add a check of the presentReceiver
token balance so that it equals PURCHASED_PRESENT_COST
, as well as add a corresponding error SantaToken__NotEnoughTokens();
and if the condition is not met, revert.
You should also add PURCHASED_PRESENT_COST
to the i_santaToken.burn()
function in the amount
parameter.
function buyPresent(address presentReceiver) external {
+ if (i_santaToken.balanceOf(presentReceiver) < PURCHASED_PRESENT_COST) {
+ revert SantaToken__NotEnoughTokens();
+ }
+ i_santaToken.burn(presentReceiver, PURCHASED_PRESENT_COST);
_mintAndIncrement();
}
contract SantaToken is ERC20 {
error SantaToken__NotSantasList();
uint8 private constant DECIMALS = 18;
+ uint64 private constant DEFAULT_VALUE = 1e18;
address private immutable i_santasList;
...
+ function burn(address from, uint256 amount) external {
if (msg.sender != i_santasList) {
revert SantaToken__NotSantasList();
}
+ _burn(from, amount);
}