diff --git a/protocol/contracts/interfaces/templegold/ISpiceAuction.sol b/protocol/contracts/interfaces/templegold/ISpiceAuction.sol index 4274ff59b..170b474e1 100644 --- a/protocol/contracts/interfaces/templegold/ISpiceAuction.sol +++ b/protocol/contracts/interfaces/templegold/ISpiceAuction.sol @@ -89,4 +89,11 @@ interface ISpiceAuction is IAuctionBase { * @param _daoExecutor New dao executor */ function setDaoExecutor(address _daoExecutor) external; + + /** + * @notice Recover auction tokens for epoch with zero bids + * @param epochId Epoch Id + * @param to Recipient + */ + function recoverAuctionTokenForZeroBidAuction(uint256 epochId, address to) external; } \ No newline at end of file diff --git a/protocol/contracts/templegold/SpiceAuction.sol b/protocol/contracts/templegold/SpiceAuction.sol index c27f05eb9..d925db2bb 100644 --- a/protocol/contracts/templegold/SpiceAuction.sol +++ b/protocol/contracts/templegold/SpiceAuction.sol @@ -264,6 +264,30 @@ contract SpiceAuction is ISpiceAuction, AuctionBase { emit CommonEventsAndErrors.TokenRecovered(to, token, amount); } + /** + * @notice Recover auction tokens for epoch with zero bids + * @param epochId Epoch Id + * @param to Recipient + */ + function recoverAuctionTokenForZeroBidAuction(uint256 epochId, address to) external override onlyDAOExecutor { + if (to == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); } + // has to be valid epoch + if (epochId > _currentEpochId) { revert InvalidEpoch(); } + // epoch has to be ended + EpochInfo storage epochInfo = epochs[epochId]; + if (!epochInfo.hasEnded()) { revert AuctionActive(); } + // bid token amount for epoch has to be 0 + if (epochInfo.totalBidTokenAmount > 0) { revert InvalidOperation(); } + + SpiceAuctionConfig storage config = auctionConfigs[epochId]; + (, address auctionToken) = _getBidAndAuctionTokens(config); + uint256 amount = epochInfo.totalAuctionTokenAmount; + _totalAuctionTokenAllocation[auctionToken] -= amount; + + emit CommonEventsAndErrors.TokenRecovered(to, auctionToken, amount); + IERC20(auctionToken).safeTransfer(to, amount); + } + /** * @notice Get spice auction config for an auction * @param auctionId Id of auction diff --git a/protocol/slither.db.json b/protocol/slither.db.json index 90186eea8..f4aaa6f85 100644 --- a/protocol/slither.db.json +++ b/protocol/slither.db.json @@ -1,4 +1,1179 @@ [ + { + "elements": [ + { + "type": "function", + "name": "bid", + "source_mapping": { + "start": 9348, + "length": 728, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200 + ], + "starting_column": 5, + "ending_column": 6 + }, + "type_specific_fields": { + "parent": { + "type": "contract", + "name": "SpiceAuction", + "source_mapping": { + "start": 1330, + "length": 14821, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225, + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245, + 246, + 247, + 248, + 249, + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257, + 258, + 259, + 260, + 261, + 262, + 263, + 264, + 265, + 266, + 267, + 268, + 269, + 270, + 271, + 272, + 273, + 274, + 275, + 276, + 277, + 278, + 279, + 280, + 281, + 282, + 283, + 284, + 285, + 286, + 287, + 288, + 289, + 290, + 291, + 292, + 293, + 294, + 295, + 296, + 297, + 298, + 299, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313, + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321, + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329, + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337, + 338, + 339, + 340, + 341, + 342, + 343, + 344 + ], + "starting_column": 1, + "ending_column": 0 + } + }, + "signature": "bid(uint256)" + } + }, + { + "type": "node", + "name": "IERC20(bidToken).safeTransferFrom(msg.sender,_recipient,amount)", + "source_mapping": { + "start": 9857, + "length": 65, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 195 + ], + "starting_column": 9, + "ending_column": 74 + }, + "type_specific_fields": { + "parent": { + "type": "function", + "name": "bid", + "source_mapping": { + "start": 9348, + "length": 728, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200 + ], + "starting_column": 5, + "ending_column": 6 + }, + "type_specific_fields": { + "parent": { + "type": "contract", + "name": "SpiceAuction", + "source_mapping": { + "start": 1330, + "length": 14821, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225, + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245, + 246, + 247, + 248, + 249, + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257, + 258, + 259, + 260, + 261, + 262, + 263, + 264, + 265, + 266, + 267, + 268, + 269, + 270, + 271, + 272, + 273, + 274, + 275, + 276, + 277, + 278, + 279, + 280, + 281, + 282, + 283, + 284, + 285, + 286, + 287, + 288, + 289, + 290, + 291, + 292, + 293, + 294, + 295, + 296, + 297, + 298, + 299, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313, + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321, + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329, + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337, + 338, + 339, + 340, + 341, + 342, + 343, + 344 + ], + "starting_column": 1, + "ending_column": 0 + } + }, + "signature": "bid(uint256)" + } + } + }, + "additional_fields": { + "underlying_type": "external_calls" + } + }, + { + "type": "node", + "name": "info.totalBidTokenAmount += amount", + "source_mapping": { + "start": 9984, + "length": 34, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 198 + ], + "starting_column": 9, + "ending_column": 43 + }, + "type_specific_fields": { + "parent": { + "type": "function", + "name": "bid", + "source_mapping": { + "start": 9348, + "length": 728, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200 + ], + "starting_column": 5, + "ending_column": 6 + }, + "type_specific_fields": { + "parent": { + "type": "contract", + "name": "SpiceAuction", + "source_mapping": { + "start": 1330, + "length": 14821, + "filename_relative": "contracts/templegold/SpiceAuction.sol", + "filename_absolute": "/Users/pb/code/delete_later/audit_fix_m09/temple/protocol/contracts/templegold/SpiceAuction.sol", + "filename_short": "contracts/templegold/SpiceAuction.sol", + "is_dependency": false, + "lines": [ + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225, + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245, + 246, + 247, + 248, + 249, + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257, + 258, + 259, + 260, + 261, + 262, + 263, + 264, + 265, + 266, + 267, + 268, + 269, + 270, + 271, + 272, + 273, + 274, + 275, + 276, + 277, + 278, + 279, + 280, + 281, + 282, + 283, + 284, + 285, + 286, + 287, + 288, + 289, + 290, + 291, + 292, + 293, + 294, + 295, + 296, + 297, + 298, + 299, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313, + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321, + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329, + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337, + 338, + 339, + 340, + 341, + 342, + 343, + 344 + ], + "starting_column": 1, + "ending_column": 0 + } + }, + "signature": "bid(uint256)" + } + } + }, + "additional_fields": { + "underlying_type": "variables_written", + "variable_name": "epochs" + } + } + ], + "description": "Reentrancy in SpiceAuction.bid(uint256) (contracts/templegold/SpiceAuction.sol#184-200):\n\tExternal calls:\n\t- IERC20(bidToken).safeTransferFrom(msg.sender,_recipient,amount) (contracts/templegold/SpiceAuction.sol#195)\n\tState variables written after the call(s):\n\t- info.totalBidTokenAmount += amount (contracts/templegold/SpiceAuction.sol#198)\n\tAuctionBase.epochs (contracts/templegold/AuctionBase.sol#11) can be used in cross function reentrancies:\n\t- SpiceAuction.bid(uint256) (contracts/templegold/SpiceAuction.sol#184-200)\n\t- SpiceAuction.claim(uint256) (contracts/templegold/SpiceAuction.sol#206-223)\n\t- SpiceAuction.getClaimableForEpoch(address,uint256) (contracts/templegold/SpiceAuction.sol#323-328)\n\t- AuctionBase.getEpochInfo(uint256) (contracts/templegold/AuctionBase.sol#19-21)\n\t- SpiceAuction.recoverAuctionTokenForZeroBidAuction(uint256,address) (contracts/templegold/SpiceAuction.sol#272-289)\n\t- SpiceAuction.recoverToken(address,address,uint256) (contracts/templegold/SpiceAuction.sol#231-265)\n\t- SpiceAuction.removeAuctionConfig() (contracts/templegold/SpiceAuction.sol#108-134)\n\t- SpiceAuction.setAuctionConfig(ISpiceAuction.SpiceAuctionConfig) (contracts/templegold/SpiceAuction.sol#85-105)\n\t- SpiceAuction.startAuction() (contracts/templegold/SpiceAuction.sol#139-177)\n", + "markdown": "Reentrancy in [SpiceAuction.bid(uint256)](contracts/templegold/SpiceAuction.sol#L184-L200):\n\tExternal calls:\n\t- [IERC20(bidToken).safeTransferFrom(msg.sender,_recipient,amount)](contracts/templegold/SpiceAuction.sol#L195)\n\tState variables written after the call(s):\n\t- [info.totalBidTokenAmount += amount](contracts/templegold/SpiceAuction.sol#L198)\n\t[AuctionBase.epochs](contracts/templegold/AuctionBase.sol#L11) can be used in cross function reentrancies:\n\t- [SpiceAuction.bid(uint256)](contracts/templegold/SpiceAuction.sol#L184-L200)\n\t- [SpiceAuction.claim(uint256)](contracts/templegold/SpiceAuction.sol#L206-L223)\n\t- [SpiceAuction.getClaimableForEpoch(address,uint256)](contracts/templegold/SpiceAuction.sol#L323-L328)\n\t- [AuctionBase.getEpochInfo(uint256)](contracts/templegold/AuctionBase.sol#L19-L21)\n\t- [SpiceAuction.recoverAuctionTokenForZeroBidAuction(uint256,address)](contracts/templegold/SpiceAuction.sol#L272-L289)\n\t- [SpiceAuction.recoverToken(address,address,uint256)](contracts/templegold/SpiceAuction.sol#L231-L265)\n\t- [SpiceAuction.removeAuctionConfig()](contracts/templegold/SpiceAuction.sol#L108-L134)\n\t- [SpiceAuction.setAuctionConfig(ISpiceAuction.SpiceAuctionConfig)](contracts/templegold/SpiceAuction.sol#L85-L105)\n\t- [SpiceAuction.startAuction()](contracts/templegold/SpiceAuction.sol#L139-L177)\n", + "first_markdown_element": "contracts/templegold/SpiceAuction.sol#L184-L200", + "id": "3f938ce880565316ffb0b9fcd5bf204d277a53db0dbf7640bc82684a03c149fe", + "check": "reentrancy-no-eth", + "impact": "Medium", + "confidence": "Medium" + }, { "elements": [ { diff --git a/protocol/test/forge/templegold/SpiceAuction.t.sol b/protocol/test/forge/templegold/SpiceAuction.t.sol index 013281795..1b1534ce0 100644 --- a/protocol/test/forge/templegold/SpiceAuction.t.sol +++ b/protocol/test/forge/templegold/SpiceAuction.t.sol @@ -302,6 +302,57 @@ contract SpiceAuctionTest is SpiceAuctionTestBase { spice.removeAuctionConfig(); } + function test_recoverAuctionTokenForZeroBidAuction() public { + vm.startPrank(daoExecutor); + // revert, zero address + vm.expectRevert(abi.encodeWithSelector(CommonEventsAndErrors.InvalidAddress.selector)); + spice.recoverAuctionTokenForZeroBidAuction(0, address(0)); + // invalid epoch + vm.expectRevert(abi.encodeWithSelector(IAuctionBase.InvalidEpoch.selector)); + spice.recoverAuctionTokenForZeroBidAuction(1, alice); + + _startAuction(true, true); + IAuctionBase.EpochInfo memory info = spice.getEpochInfo(1); + vm.startPrank(daoExecutor); + vm.warp(info.startTime); + vm.expectRevert(abi.encodeWithSelector(IAuctionBase.AuctionActive.selector)); + spice.recoverAuctionTokenForZeroBidAuction(1, alice); + vm.startPrank(bob); + uint256 bidAmount = 10 ether; + deal(daiToken, bob, bidAmount); + IERC20(daiToken).approve(address(spice), type(uint).max); + spice.bid(bidAmount); + uint256 epochOneTotalAuctionTokenAmount = info.totalAuctionTokenAmount; + ISpiceAuction.SpiceAuctionConfig memory _config = spice.getAuctionConfig(1); + vm.warp(info.endTime + _config.waitPeriod); + // fail for epoch with bid + vm.startPrank(daoExecutor); + vm.expectRevert(abi.encodeWithSelector(IAuctionBase.InvalidOperation.selector)); + spice.recoverAuctionTokenForZeroBidAuction(1, alice); + + _startAuction(true, true); + info = spice.getEpochInfo(2); + _config = spice.getAuctionConfig(2); + vm.warp(info.endTime + _config.waitPeriod); + address auctionToken = spice.getAuctionTokenForCurrentEpoch(); + uint256 auctionTokenBalance = IERC20(auctionToken).balanceOf(address(spice)); + uint256 auctionTokenAmount = info.totalAuctionTokenAmount; + uint256 aliceBalance = IERC20(auctionToken).balanceOf(alice); + vm.startPrank(daoExecutor); + + vm.expectEmit(address(spice)); + emit TokenRecovered(alice, auctionToken, auctionTokenAmount); + spice.recoverAuctionTokenForZeroBidAuction(2, alice); + assertEq(IERC20(auctionToken).balanceOf(address(spice)), auctionTokenBalance - auctionTokenAmount); + assertEq(IERC20(auctionToken).balanceOf(alice), aliceBalance + auctionTokenAmount); + + // bidders from previous auction can claim + vm.startPrank(bob); + uint256 bobBalance = IERC20(auctionToken).balanceOf(bob); + spice.claim(1); + assertEq(IERC20(auctionToken).balanceOf(bob), bobBalance + epochOneTotalAuctionTokenAmount); + } + function test_recoverToken_spice() public { vm.startPrank(daoExecutor); address _fakeErc20TokenAddress = address(fakeERC20);