Platform: Code4rena
Start Date: 13/12/2023
Pot Size: $36,500 USDC
Total HM: 18
Participants: 110
Period: 8 days
Judge: 0xTheC0der
Id: 311
League: ETH
Rank: 26/110
Findings: 6
Award: $238.23
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: osmanozdemir1
Also found by: 0xCiphky, 0xDING99YA, 0xlemon, 0xluckhu, AS, Abdessamed, BARW, KupiaSec, MrPotatoMagic, SovaSlava, SpicyMeatball, ast3ros, bart1e, hakymulla, ktg, n1punp, plasmablocks, shaka, twcctop
44.0266 USDC - $44.03
The buyToken
function in the ERC20TokenEmitter contract is designed to send out all ETH received in a transaction (confirmed by devs). However, a portion of the ETH is inadvertently left in the contract, which subsequently becomes locked as there is no mechanism to withdraw it.
To understand this issue, let's examine the buyToken
function, focusing on how ETH and tokens are distributed from a transaction.
The function first allocates protocol rewards, which are 2.5% of the total transaction value.
function buyToken( ... ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { ... // Get value left after protocol rewards uint256 msgValueRemaining = _handleRewardsAndGetValueToSend( msg.value, protocolRewardsRecipients.builder, protocolRewardsRecipients.purchaseReferral, protocolRewardsRecipients.deployer ); ... }
Next, the amount of ETH to pay the treasury is calculated based on creatorRateBps
.
function buyToken( ... ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { ... //Share of purchase amount to send to treasury uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000; ... }
The creator's share is then divided into ETH (creatorDirectPayment) and tokens. The ETH part is sent directly to the creator, while the token part is minted accordingly.
function buyToken( ... ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { ... //Share of purchase amount to reserve for creators //Ether directly sent to creators uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000; //Tokens to emit to creators int256 totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0 ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment) : int256(0); ... }
The final step involves calculating the buyer's tokens, which is equivalent to the treasury’s share in ETH.
function buyToken( ... ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { ... // Tokens to emit to buyers int256 totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int256(0); ... }
In summary, the total ETH received is divided among protocol rewards, the creator's share, and the treasury. Focusing specifically on the creator's share, it is further split into two components: the creatorDirectPayment
and the totalTokensForCreators
. While the creatorDirectPayment
is directly sent to the creator in ETH, the totalTokensForCreators
are allocated in the form of minted tokens. However, the issue arises with the ETH value corresponding to these totalTokensForCreators
. This portion of ETH, despite being accounted for in tokens, remains unallocated and unused within the contract.
function testBuyTokenRemainingETH() public { vm.startPrank(address(0)); address[] memory recipients = new address[](1); recipients[0] = address(1); uint256[] memory bps = new uint256[](1); bps[0] = 10_000; vm.deal(address(0), 100000 ether); vm.stopPrank(); // set setCreatorsAddress vm.prank(address(dao)); erc20TokenEmitter.setCreatorsAddress(address(100)); // change creatorRateBps to 1000 vm.prank(address(dao)); erc20TokenEmitter.setCreatorRateBps(1000); // set entropyRateBps to 1000 vm.prank(address(dao)); erc20TokenEmitter.setEntropyRateBps(1000); assertEq(address(erc20TokenEmitter).balance, 0, "Contract starts with 0 balance"); vm.startPrank(address(0)); erc20TokenEmitter.buyToken{value: 1e18}( recipients, bps, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); // creator bal + treasury bal + remaining eth in contract + protocol fees = 1e18 assertEq( address(erc20TokenEmitter.creatorsAddress()).balance + address(erc20TokenEmitter.treasury()).balance + address(erc20TokenEmitter).balance + 2.5e16, 1e18, "Creator balance + treasury balance + remaining eth should be 1e18" ); // remaining eth in contract greater than 0 assertGt(address(erc20TokenEmitter).balance, 0, "Remaining ETH should be 0"); }
A portion of the ETH, instead of being distributed, remains within the contract with no defined pathway for its release. This results in the ETH being effectively stuck in the contract.
buyToken
function is used often, which means this issue will likely happen repeatedly.Revise the function's logic to guarantee that all ETH received by the contract is fully distributed in each transaction, specifically addressing the ETH equivalent of the totalTokensForCreators
.
Other
#0 - c4-pre-sort
2023-12-22T06:51:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T06:51:21Z
raymondfam marked the issue as duplicate of #13
#2 - c4-pre-sort
2023-12-24T02:55:16Z
raymondfam marked the issue as duplicate of #406
#3 - c4-judge
2024-01-05T23:18:36Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: jnforja
Also found by: 0x175, 0xCiphky, 0xDING99YA, 0xmystery, ArmedGoose, Aymen0909, Franklin, KupiaSec, McToady, MrPotatoMagic, Ocean_Sky, PNS, Pechenite, TermoHash, Topmark, _eperezok, alexbabits, deth, hals, imare, jeff, ktg, leegh, mahdirostami, marqymarq10, mojito_auditor, neocrao, ptsanev, twcctop, zraxx
1.337 USDC - $1.34
Judge has assessed an item in Issue #341 as 2 risk. The relevant finding follows:
L03: Auction parameters can be changed during an auction
#0 - c4-judge
2024-01-11T19:29:58Z
MarioPoneder marked the issue as duplicate of #515
#1 - c4-judge
2024-01-11T19:30:02Z
MarioPoneder marked the issue as partial-50
🌟 Selected for report: deth
Also found by: 00xSEV, 0xCiphky, 0xHelium, ABAIKUNANBAEV, Aamir, AkshaySrivastav, King_, Pechenite, SpicyMeatball, Tricko, ast3ros, ayden, bart1e, deth, fnanni, ke1caM, mahdirostami, peanuts, pep7siup, pontifex, ptsanev, roland, rvierdiiev, y4y
7.2215 USDC - $7.22
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L209 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L307
The CultureIndex contract permits the creation of new art pieces through the createPiece function. These pieces are subject to voting and potential minting based on the highest votes received. Voting on an art piece is only permitted if a user owned or was delegated the respective ERC20 tokens of ERC721 token during or before the creation of the respective art piece.
function createPiece(ArtPieceMetadata calldata metadata, CreatorBps[] calldata creatorArray) public returns (uint256) { ... ArtPiece storage newPiece = pieces[pieceId]; newPiece.pieceId = pieceId; newPiece.totalVotesSupply = _calculateVoteWeight(erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply()); newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); newPiece.metadata = metadata; newPiece.sponsor = msg.sender; newPiece.creationBlock = block.number; newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000; ... }
While this voting system safeguards against certain attacks, it inadvertently creates a disadvantage for pieces created earlier. Art pieces minted at different times can accumulate differing maximum vote counts, giving a distinct advantage to pieces created later, as the total token supply increases over time.
Implement a feature that permits creators of early art pieces, particularly those that have not yet been voted to the top, to delete and recreate their submissions. This would enable these pieces to be reintroduced with a vote capacity aligned with the current total token supply, thereby levelling the playing field.
Other
#0 - c4-pre-sort
2023-12-22T07:06:33Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T07:06:56Z
raymondfam marked the issue as duplicate of #16
#2 - c4-pre-sort
2023-12-24T15:11:10Z
raymondfam marked the issue as duplicate of #449
#3 - c4-judge
2024-01-06T16:00:03Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: deth
Also found by: 00xSEV, 0xCiphky, 0xHelium, ABAIKUNANBAEV, Aamir, AkshaySrivastav, King_, Pechenite, SpicyMeatball, Tricko, ast3ros, ayden, bart1e, deth, fnanni, ke1caM, mahdirostami, peanuts, pep7siup, pontifex, ptsanev, roland, rvierdiiev, y4y
7.2215 USDC - $7.22
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L307 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L209
The CultureIndex contract facilitates a snapshot approach voting mechanism for art pieces. This method allows users to vote on art pieces using the ERC20 and ERC721 tokens they held or were delegated at or before the specific snapshot time.
Furthermore, the protocol implements a quorum requirement for art pieces to progress to the auction stage. This requirement ensures that a piece must reach a community consensus before it can be minted. The quorum threshold is influenced by the total vote supply available at the moment the art piece is created.
While effective in scenarios with sufficient supply, this system can encounter problems when the token supply is minimal or nonexistent.
function createPiece(ArtPieceMetadata calldata metadata, CreatorBps[] calldata creatorArray) public returns (uint256) { ... newPiece.totalVotesSupply = _calculateVoteWeight(erc20VotingToken.totalSupply(), erc721VotingToken.totalSupply()); newPiece.totalERC20Supply = erc20VotingToken.totalSupply(); newPiece.metadata = metadata; newPiece.sponsor = msg.sender; newPiece.creationBlock = block.number; newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000; ... }
In cases where the total vote supply is zero, the quorum becomes ineffective, leading to potential bypassing of quorum restrictions. Additionally, these pieces will be ineligible for voting due to the time-based snapshot voting mechanism.
When the total vote supply is low, it can lead to an art piece being indefinitely locked from reaching the auction stage due to the quorum threshold. Specific scenarios contributing to this problem include:
This issue could make it difficult for early creators to meet the quorum threshold or compete with later creators, as their maximum achievable votes are limited and can voting be more easily influenced when the total vote supply is low.
Ensure that the ERC20TokenEmitter contract is deployed and operational prior to enabling the creation of art pieces. This allows the token to reach a certain supply threshold and a diverse holder base, mitigating the issues associated with low supply.
Other
#0 - c4-pre-sort
2023-12-22T07:07:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T07:07:50Z
raymondfam marked the issue as duplicate of #16
#2 - c4-pre-sort
2023-12-24T15:11:12Z
raymondfam marked the issue as duplicate of #449
#3 - c4-judge
2024-01-06T16:00:23Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: ayden
Also found by: 0xCiphky, AS, Brenzee, Inference, KupiaSec, SpicyMeatball, ast3ros, ayden, fnanni, hals, ktg, mahdirostami, nmirchev8, rvierdiiev, wangxx2026
29.8238 USDC - $29.82
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/AuctionHouse.sol#L336 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/AuctionHouse.sol#L253
The _settleAuction function in the AuctionHouse contract is responsible for finalizing auctions, transferring art pieces to the highest bidder, and paying out the creator and owner. Generally, this function operates as intended, but it fails to handle an edge case where entropyRateBps is set to zero.
The _settleAuction function calls the buyToken function from the ERC20TokenEmitter contract to purchase tokens and pay the creators of the art piece. The vrgdaSplits and vrgdaReceivers arrays, which are essential for this process, are only set if the condition “creatorsShare > 0 && entropyRateBps > 0” is met. If entropyRateBps is zero, indicating that creators are to be paid entirely in tokens, these arrays remain uninitialized. This results in them being passed with default (zero) values to the buyToken function.
function _settleAuction() internal { ... //Build arrays for erc20TokenEmitter.buyToken uint256[] memory vrgdaSplits = new uint256[](numCreators); address[] memory vrgdaReceivers = new address[](numCreators); ... uint256 ethPaidToCreators = 0; //Transfer creator's share to the creator, for each creator, and build arrays for erc20TokenEmitter.buyToken if (creatorsShare > 0 && entropyRateBps > 0) { for (uint256 i = 0; i < numCreators; i++) { ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i]; vrgdaReceivers[i] = creator.creator; vrgdaSplits[i] = creator.bps; ... } } //Buy token from ERC20TokenEmitter for all the creators if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{value: creatorsShare - ethPaidToCreators}( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer ... }
As a consequence, the erc20TokenEmitter.buyToken function will always revert when attempting to mint tokens for creators, because its internal mint function reverts when the receiver is the zero address.
function _mint(address account, uint256 value) internal override { if (account == address(0)) { revert ERC20InvalidReceiver(address(0)); } _update(address(0), account, value); }
Given that the setEntropyRateBps function allows for a zero value, it's a valid edge case that should be accounted for in the _settleAuction function.
function setEntropyRateBps(uint256 _entropyRateBps) external onlyOwner { require(_entropyRateBps <= 10_000, "Entropy rate must be less than or equal to 10_000"); entropyRateBps = _entropyRateBps; emit EntropyRateBpsUpdated(_entropyRateBps); }
function testSettleAuctionZeroEntropyRate() public { // set entropy rate to 0 auction.setEntropyRateBps(0); createDefaultArtPiece(); auction.unpause(); address recipient = address(0x123); // Some EOA address uint256 amount = 1 ether; auction.transferOwnership(recipient); vm.startPrank(recipient); auction.acceptOwnership(); vm.startPrank(address(auction)); vm.deal(address(auction), amount); auction.createBid{value: amount}(0, address(this)); // Assuming first auction's verbId is 0 //go in future vm.warp(block.timestamp + auction.duration() + 1); // Fast forward time to end the auction // reverts as creator array not set, zero address vm.expectRevert(abi.encodeWithSelector(ERC20InvalidReceiver.selector, address(0))); auction.settleCurrentAndCreateNewAuction(); }
If the entropyRateBps is set to zero, the _settleAuction function will invariably fail until the contract owner changes the entropyRateBps to a non-zero value.
To resolve this issue, the initialization of the vrgdaSplits and vrgdaReceivers arrays should occur outside the conditional statement that checks for entropyRateBps > 0. This ensures that even when entropyRateBps is zero, the correct addresses and splits are provided to the buyToken function, thereby allowing the function to execute successfully regardless of the entropyRateBps value.
Other
#0 - c4-pre-sort
2023-12-22T07:00:11Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T07:00:28Z
raymondfam marked the issue as primary issue
#2 - c4-sponsor
2024-01-04T22:34:08Z
rocketman-21 (sponsor) confirmed
#3 - c4-judge
2024-01-06T01:25:34Z
MarioPoneder changed the severity to QA (Quality Assurance)
#4 - c4-judge
2024-01-06T15:11:24Z
This previously downgraded issue has been upgraded by MarioPoneder
#5 - c4-judge
2024-01-06T15:12:28Z
MarioPoneder marked the issue as duplicate of #160
#6 - c4-judge
2024-01-07T00:24:13Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: Aamir
Also found by: 0xCiphky, SovaSlava, bart1e, osmanozdemir1, rvierdiiev, shaka
148.462 USDC - $148.46
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L29 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/CultureIndex.sol#L431
The CultureIndex contract allows off chain voting by users assigning their votes via signature to others. The signature is verified using a typehash that includes the user's address, specific pieceIds, nonce, and deadline. The trusted party/user can then vote on behalf of users.
Per the EIP-712 standard, arrays should be encoded by concatenating their elements and hashing the result with keccak256:
The array values are encoded as the keccak256 hash of the concatenated encodeData of their contents (i.e. the encoding of SomeType[5] is identical to that of a struct containing five members of type SomeType).
However, the current implementation in the contract does not encode the array in this manner. Instead, the pieceIds array is passed directly:
function _verifyVoteSignature( ... ) internal returns (bool success) { ... voteHash = keccak256(abi.encode(VOTE_TYPEHASH, from, pieceIds, nonces[from]++, deadline)); ... }
As a result, the signature verification process is not compliant with EIP-712. Users, contracts, or dApps encoding arrays as per EIP-712 specifications will produce different signatures, leading to a failure of the above functions when executed.
To rectify this issue and align with EIP-712 standards, the signature verification process in the contract should be updated to encode arrays correctly as outlined in EIP-712.
Other
#0 - c4-pre-sort
2023-12-22T07:05:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T07:05:49Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-01-06T15:19:19Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: 0xmystery
Also found by: 00xSEV, 0xCiphky, 0xDING99YA, 0xhitman, ABAIKUNANBAEV, Aamir, BARW, IllIllI, King_, MrPotatoMagic, Pechenite, SovaSlava, SpicyMeatball, Topmark, Ward, ZanyBonzy, ast3ros, bart1e, cheatc0d3, deth, developerjordy, hals, imare, kaveyjoe, ktg, leegh, osmanozdemir1, peanuts, roland, rvierdiiev, shaka, sivanesh_808, spacelord47
7.359 USDC - $7.36
ID | Title | Severity |
---|---|---|
L01 | Metadata not properly validated | Low Risk |
L02 | Immutable variables not preserved between upgrades | Low Risk |
L03 | Auction parameters can be changed during an auction | Low Risk |
L04 | Revert state in auction due to computeTotalReward function failure | Low Risk |
The CultureIndex contract permits the creation of new art pieces through the createPiece function. These pieces are subject to voting and potential minting based on the highest votes received. The createPiece function sets forth certain requirements for the art piece's metadata:
Requirements:
However, the createPiece function currently lacks verification for the presence of a name and description in the metadata fields.
function createPiece(ArtPieceMetadata calldata metadata, CreatorBps[] calldata creatorArray) public returns (uint256) { uint256 creatorArrayLength = validateCreatorsArray(creatorArray); // Validate the media type and associated data validateMediaType(metadata); ... }
Additionally, the validateMediaType function is called within createPiece to ensure requirements are met depending on the MediaType chosen. However, it does not cover all cases. A user can select from the following types:
// Enum representing different media types for art pieces. enum MediaType { NONE, IMAGE, ANIMATION, AUDIO, TEXT, OTHER }
While the IMAGE, ANIMATION, and TEXT fields are checked to ensure a valid value is provided, the AUDIO type, which should fall under ANIMATION, is not checked.
/** * Requirements: * - The media type must be one of the defined types in the MediaType enum. * - The corresponding media data must not be empty. */ function validateMediaType(ArtPieceMetadata calldata metadata) internal pure { require(uint8(metadata.mediaType) > 0 && uint8(metadata.mediaType) <= 5, "Invalid media type"); if (metadata.mediaType == MediaType.IMAGE) { require(bytes(metadata.image).length > 0, "Image URL must be provided"); } else if (metadata.mediaType == MediaType.ANIMATION) { require(bytes(metadata.animationUrl).length > 0, "Animation URL must be provided"); } else if (metadata.mediaType == MediaType.TEXT) { require(bytes(metadata.text).length > 0, "Text must be provided"); } }
As a result, it is currently possible for art pieces to be created with missing fields in the metadata. Furthermore, as these pieces are auctioned off, an art piece that doesn’t follow the protocol's requirements could inadvertently be auctioned off for a substantial amount.
Add the following requirements to ensure the metadata requirements are followed by users when creating art pieces
function createPiece(ArtPieceMetadata calldata metadata, CreatorBps[] calldata creatorArray) public returns (uint256) { require(bytes(metadata.name).length > 0, "name must be provided"); //add here require(bytes(metadata.description).length > 0, "description must be provided"); //add here ... }
function validateMediaType(ArtPieceMetadata calldata metadata) internal pure { require(uint8(metadata.mediaType) > 0 && uint8(metadata.mediaType) <= 5, "Invalid media type"); if (metadata.mediaType == MediaType.IMAGE) { require(bytes(metadata.image).length > 0, "Image URL must be provided"); } else if (metadata.mediaType == MediaType.ANIMATION || metadata.mediaType == MediaType.AUDIO) { //add here require(bytes(metadata.animationUrl).length > 0, "Animation URL must be provided"); } else if (metadata.mediaType == MediaType.TEXT) { require(bytes(metadata.text).length > 0, "Text must be provided"); } }
The protocol employs the following upgradeable contracts: CultureIndex, MaxHeap, Descriptor, AuctionHouse, and VerbsToken. Despite these contracts being upgradeable they still initialize immutable variables through constructors.
This approach is not recommended for proxy contracts because immutable variables do not persist through upgrades. If the implementation contract is upgraded without inheriting the first version or integrating the immutable variables into the new implementation, they will not be available in the new version, unlike storage variables that reside in the proxy contract.
/// @notice The contract upgrade manager IRevolutionBuilder public immutable manager; /// /// /// CONSTRUCTOR /// /// /// /// @param _manager The contract upgrade manager address constructor(address _manager) payable initializer { manager = IRevolutionBuilder(_manager); }
This design can lead to unexpected behaviour in the upgraded contract versions, if the immutable values set in the original contract is not carried over.
Avoid using constructors for setting crucial contract parameters, instead use the initializer function to set necessary values.
The AuctionHouse contract is responsible for minting top-voted VerbsTokens and initiating auctions for them. Participants can bid on these auctions, and the highest bidder receives the art piece. The auction proceeds are then divided between the creators of the art piece and the owner of the auction contract.
function settleCurrentAndCreateNewAuction() external override nonReentrant whenNotPaused { _settleAuction(); _createAuction(); }
An auction is concluded by invoking the settleCurrentAndCreateNewAuction function after the condition “block.timestamp >= _auction.endTime” is met. The _settleAuction function is responsible for transferring the art piece and distributing payments if all conditions are fulfilled. One such condition is that the contract's balance must be greater than the reserve price, signifying that a valid bid has been placed.
function _settleAuction() internal { IAuctionHouse.Auction memory _auction = auction; require(_auction.startTime != 0, "Auction hasn't begun"); require(!_auction.settled, "Auction has already been settled"); //slither-disable-next-line timestamp require(block.timestamp >= _auction.endTime, "Auction hasn't completed"); auction.settled = true; uint256 creatorTokensEmitted = 0; // Check if contract balance is greater than reserve price if (address(this).balance < reservePrice) { // If contract balance is less than reserve price, refund to the last bidder if (_auction.bidder != address(0)) { _safeTransferETHWithFallback(_auction.bidder, _auction.amount); } // And then burn the Noun verbs.burn(_auction.verbId); } else { ... }
However, as it stands the contract owner can change the reserve price at any time using the setReservePrice function. This allows them to potentially block the highest bidder from winning by setting the reserve price higher than the contract's balance, leading to the burning of the piece and refunding the bidder.
function setReservePrice(uint256 _reservePrice) external override onlyOwner { reservePrice = _reservePrice; emit AuctionReservePriceUpdated(_reservePrice); }
Severity: Low. While this ability to change the reserve price mid-auction could be misused to unfairly influence auction outcomes, its impact is limited given the owner address is a trusted role.
To enhance security, the trusted address in the contract, particularly for sensitive functions like setting auction parameters, should be managed through a multi-signature wallet. This approach ensures that important decisions require consensus among multiple trusted parties, reducing the risk of unauthorized changes due to a compromised account.
The _settleAuction function in the AuctionHouse contract is responsible for finalizing auctions, transferring art pieces to the highest bidder, and paying out the creator and owner. The function has implemented a fail safe method by pausing the contract when the _settleAuction function reverts.
The auction is concluded using the settleCurrentAndCreateNewAuction function, which is triggered after the condition “block.timestamp >= _auction.endTime” is met. Within this process, the _settleAuction function handles the transfer of the art piece and the distribution of payments.
function _settleAuction() internal { ... //Buy token from ERC20TokenEmitter for all the creators if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{value: creatorsShare - ethPaidToCreators}( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer ... }
The buyToken function is internally called, it purchases tokens for creators and the buyer. This function includes a step where 2.5% of the purchase is allocated as rewards, calculated by the computeTotalReward function.
function buyToken( ... ) public payable nonReentrant whenNotPaused returns (uint256 tokensSoldWad) { ... // Get value left after protocol rewards uint256 msgValueRemaining = _handleRewardsAndGetValueToSend( msg.value, protocolRewardsRecipients.builder, protocolRewardsRecipients.purchaseReferral, protocolRewardsRecipients.deployer ); ... }
This function is designed with specific constraints: if the paymentAmountWei falls below the minimum threshold (0.0000001 ether) or exceeds the maximum limit (50,000 ether), the function will trigger a revert. This can lead to potential issues in certain transaction scenarios.
function computeTotalReward(uint256 paymentAmountWei) public pure returns (uint256) { if (paymentAmountWei <= minPurchaseAmount || paymentAmountWei >= maxPurchaseAmount) revert INVALID_ETH_AMOUNT(); ... }
While a direct call to buyToken
simply reverts the transaction, a call through _settleAuction
can lead to the auction being locked until the auction's split percentages are adjusted or the tokens are burnt.
Locking of Auctions: This issue can result in auctions being locked and unable to proceed, requiring manual intervention to resolve
Implement a fallback mechanism in the _settleAuction
function to handle amounts outside the desired range.
#0 - raymondfam
2023-12-24T17:34:40Z
Possible upgrades:
L-01 --> #167 L-03 --> #495
#1 - c4-pre-sort
2023-12-24T17:34:45Z
raymondfam marked the issue as sufficient quality report
#2 - MarioPoneder
2024-01-07T18:51:43Z
Possible upgrades:
L-01 --> #167 L-03 --> #495
L-01: Rather #151, now QA L-03: #495 is now QA
#3 - c4-judge
2024-01-07T19:42:38Z
MarioPoneder marked the issue as grade-b