Golom contest - 0xsanson's results

An NFT marketplace that offers the lowest industry fee, a publicly available order-book along with analytical tools.

General Information

Platform: Code4rena

Start Date: 26/07/2022

Pot Size: $75,000 USDC

Total HM: 29

Participants: 179

Period: 6 days

Judge: LSDan

Total Solo HM: 6

Id: 148

League: ETH

Golom

Findings Distribution

Researcher Performance

Rank: 7/179

Findings: 13

Award: $2,592.80

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Awards

93.2805 USDC - $93.28

Labels

bug
duplicate
3 (High Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/rewards/RewardDistributor.sol#L300

Vulnerability details

Impact

Starting from ve == 0, we call addVoteEscrow.

    function addVoteEscrow(address _voteEscrow) external onlyOwner {
        if (address(ve) == address(0)) {
            ve = VE(pendingVoteEscrow);
        } else {
            voteEscrowEnableDate = block.timestamp + 1 days;
            pendingVoteEscrow = _voteEscrow;
        }
    }

The function sets ve to pendingVoteEscrow which is also address(0). There's no other way to change pendingVoteEscrow, so ve will always be unset.

Without ve it's impossible to claim staker rewards.

Proof of Concept

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/rewards/RewardDistributor.sol#L300

Clearly, ve = VE(_voteEscrow); at line 300.

#0 - KenzoAgada

2022-08-04T03:12:39Z

Duplicate of #611

Awards

26.7695 USDC - $26.77

Labels

bug
duplicate
3 (High Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L85 https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L101

Vulnerability details

Impact

It's impossible to delegate to a token, because the first delegation will always revert.

Consider there's no delegation so far, meaning that every token will have numCheckpoints[token] = 0. When calling delegate, we will have nCheckpoints = 0 and the call will revert in _writeCheckpoint since nCheckpoints - 1 underflows.

    function delegate(uint256 tokenId, uint256 toTokenId) external {
    	...
        uint256 nCheckpoints = numCheckpoints[toTokenId];

        if (nCheckpoints > 0) {
        	...
        } else {
            uint256[] memory array = new uint256[](1);
            array[0] = tokenId;
            _writeCheckpoint(toTokenId, nCheckpoints, array);
        }
    }
    
    function _writeCheckpoint(
        uint256 toTokenId,
        uint256 nCheckpoints,
        uint256[] memory _delegatedTokenIds
    ) internal {
        require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more');

        Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1];  //@audit Reverts here
        
        ...
    }

Proof of Concept

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L85 https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L101

Instead of

Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1];

write something like:

Checkpoint memory oldCheckpoint;
if (nCheckpoints > 0) oldCheckpoint  = checkpoints[toTokenId][nCheckpoints - 1];

#0 - KenzoAgada

2022-08-02T09:09:05Z

Duplicate of #630

Awards

26.7695 USDC - $26.77

Labels

bug
duplicate
3 (High Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L71-L89

Vulnerability details

Impact

It's possible to call delegate(tokenId1, tokenId2) using the same token tokenId1 multiple times. The previous delegated value isn't deleted, and all delegations stack up.

This way tokenId2 will have all these tokens as delegated:

checkpoints[tokenId2][nCheckpoints-1].delegatedTokenIds = [tokenId1, tokenId1, ...]

When calculating the votes for this token, there's no check that the delegated tokens are different, so the values all sum together.

Proof of Concept

Alice has two tokens. She calls delegate(tokenId1, tokenId2) multiple times, accruing the voting power of tokenId2 over her true token balance. During a governance proposal, GovernorAlpha#castVote will call VoteEscrow#getPriorVotes getting an artificially high voting power. She can use this power to pass whatever proposal she likes.

Consider adding a require(delegates[tokenId] == address(0)) in the function delegate. Changing a delegation this way requires the user to call removeDelegation beforehand.

#0 - KenzoAgada

2022-08-04T03:13:29Z

Duplicate of #169

Findings Information

🌟 Selected for report: CertoraInc

Also found by: 0xA5DF, 0xsanson, Bahurum, GalloDaSballo, MEP, TrungOre, carlitox477, cryptphi, kenzo

Labels

bug
duplicate
3 (High Risk)

Awards

280.8379 USDC - $280.84

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L210 https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L242

Vulnerability details

Impact

In VoteEscrowDelegation.sol, function _transferFrom will attempt to remove the delegations to the token transferred.

// remove the delegation
this.removeDelegation(_tokenId);

However this external call will change msg.sender to address(this), reverting the call when checking the owner

    function removeDelegation(uint256 tokenId) external {
        require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
        uint256 nCheckpoints = numCheckpoints[tokenId];
        Checkpoint storage checkpoint = checkpoints[tokenId][nCheckpoints - 1];
        removeElement(checkpoint.delegatedTokenIds, tokenId);
        _writeCheckpoint(tokenId, nCheckpoints, checkpoint.delegatedTokenIds);
    }

Also, even if the call was made internal, another revert is possible: if the token doesn't have any checkpoints yet, nCheckpoints - 1 will revert for underflow.

Proof of Concept

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L210 https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L242

  1. Change removeDelegation to public, and write removeDelegation(_tokenId) instead of this.removeDelegation(_tokenId);
  2. Add a if (nCheckpoints == 0) return; in removeDelegation before the possible underflow.

#0 - KenzoAgada

2022-08-02T05:51:56Z

First issue is duplicate of #377 Second issue is separate, needs to be deduped later

#1 - zeroexdead

2022-08-15T16:34:56Z

Second issue is duplicate of #673

#2 - dmvt

2022-10-13T12:21:16Z

Duplicate of #377

Findings Information

🌟 Selected for report: async

Also found by: 0xpiglet, 0xsanson, DimitarDimitrov, Dravee, ElKu, IllIllI, JohnSmith, ak1, kenzo, scaraven

Labels

bug
duplicate
3 (High Risk)

Awards

189.5656 USDC - $189.57

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L104

Vulnerability details

Impact

    function _writeCheckpoint(
        uint256 toTokenId,
        uint256 nCheckpoints,
        uint256[] memory _delegatedTokenIds
    ) internal {
        require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more');

        Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1];

        if (nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number) {
            oldCheckpoint.delegatedTokenIds = _delegatedTokenIds;
        } else {
            checkpoints[toTokenId][nCheckpoints] = Checkpoint(block.number, _delegatedTokenIds);
            numCheckpoints[toTokenId] = nCheckpoints + 1;
        }
    }

Inside the first if, we are writing to memory instead of storage. This means that if nCheckpoints > 0 && oldCheckpoint.fromBlock == block.number, the function will absolutely do nothing.

Proof of Concept

Alice flashloans tokenId1. She then calls delegate(tokenId1, tokenId2) where tokenId2 is a token she owns permanently. When she returns the first token, there will be an attempt to remove tokenId1 from the delegates of tokenId2 (not considering other issues in the removeDelegation function). However, since oldCheckpoint.fromBlock == block.number, _writeCheckpoint will fail to change the state.

Using this trick, Alice is able then to bypass any removeDelegation, leading to possible governance attacks.

Also the contract state will be messed up going forward, since the checkpoint of tokenId2 will have tokenId1, but delegates[tokenId1] is deleted. If delegates was used as check against delegating the same token multiple times, this exploit can be used to continue adding the same token as delegate, increasing limitlessly the governance power of an exploiter.

Checkpoint memory oldCheckpoint to storage.

#0 - KenzoAgada

2022-08-02T08:14:27Z

Duplicate of #455

Findings Information

🌟 Selected for report: kenzo

Also found by: 0xA5DF, 0xpiglet, 0xsanson, Bahurum, IllIllI, arcoun

Labels

bug
duplicate
3 (High Risk)

Awards

550.3388 USDC - $550.34

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L79-L80

Vulnerability details

Impact

VoteEscrow tokens are used as voting tokens for a GovernorAlpha governance. It shouldn't be possible to modify an old (meaning for blocks older than block.number) checkpoint, otherwise it's possible to buy tokens just to vote for a proposal and then sell them.

In delegate function however there's such a bug.

    function delegate(uint256 tokenId, uint256 toTokenId) external {
        require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
        require(this.balanceOfNFT(tokenId) >= MIN_VOTING_POWER_REQUIRED, 'VEDelegation: Need more voting power');

        delegates[tokenId] = toTokenId;
        uint256 nCheckpoints = numCheckpoints[toTokenId];

        if (nCheckpoints > 0) {
            Checkpoint storage checkpoint = checkpoints[toTokenId][nCheckpoints - 1];
-->         checkpoint.delegatedTokenIds.push(tokenId);
            _writeCheckpoint(toTokenId, nCheckpoints, checkpoint.delegatedTokenIds);
        } 
        ...
    }

_writeCheckpoint correctly makes a new checkpoint for the correct block number, but before that the code adds a new token to the last checkpoint, which has an older block number.

Proof of Concept

A new governance proposal started. Alice has already a token id1. She can buy (and later sell) a token id2. Calling delegate(id2, id1) she adds id2 to her delegations, and she's able to vote also using its balance.

Let's say she also has a token id3 before the proposal started. She can call delegate(id2, id3) (deleting id2 delegates beforehand) and id2 will also count when she votes using id3 (it's basically a double count for id2). The process can of course be repeated with multiple tokens, leading to abusers getting way more voting powers than normal.

Checkpoint storage checkpoint = checkpoints[toTokenId][nCheckpoints - 1]; to memory.

#0 - KenzoAgada

2022-08-02T07:55:25Z

Duplicate of #81

Findings Information

Awards

93.2805 USDC - $93.28

Labels

bug
duplicate
3 (High Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L210-L216

Vulnerability details

Impact

The function removeDelegation(tokenId) currently tries to remove tokenId from the list of tokens delegated to itself.

The correct behavior would be to get which token tokenId is delegated to, and remove tokenId from the delegations to that token.

Since removeDelegation is important in resetting delegations during token transfers, this malfunctions allows some users to gain more governance votes.

Proof of Concept

Alice flashloans (or simply loans) some tokens with high balance and, before returning them, she delegate their voting power to another token she controls. Since removeDelegation malfunctions, she keeps the votes of all these tokens, leading to a possible governance attack.

Use delegates[tokenId] to get the correct token from which getting the checkpoint.

    function removeDelegation(uint256 tokenId) external {
        require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed');
        uint256 toTokenId = delegates[tokenId];      //@audit Add this
        //uint256 nCheckpoints = numCheckpoints[tokenId];
        uint256 nCheckpoints = numCheckpoints[toTokenId];
        //Checkpoint storage checkpoint = checkpoints[tokenId][nCheckpoints - 1];
        Checkpoint storage checkpoint = checkpoints[toTokenId][nCheckpoints - 1]; 
        removeElement(checkpoint.delegatedTokenIds, tokenId);
        //_writeCheckpoint(tokenId, nCheckpoints, checkpoint.delegatedTokenIds);
        _writeCheckpoint(toTokenId, nCheckpoints, checkpoint.delegatedTokenIds);
    }

#0 - KenzoAgada

2022-08-02T08:22:39Z

Duplicate of #751

Judge has assessed an item in Issue #960 as Medium risk. The relevant finding follows:

#0 - dmvt

2022-10-21T13:31:47Z

Findings Information

🌟 Selected for report: codexploder

Also found by: 0x1f8b, 0xNineDec, 0xsanson, RustyRabbit, Treasure-Seeker, berndartmueller, chatch, teddav

Labels

bug
duplicate
2 (Med Risk)
upgraded by judge

Awards

104.014 USDC - $104.01

External Links

Judge has assessed an item in Issue #960 as Medium risk. The relevant finding follows:

[L5] chainId constant

In GolomTrader, EIP712_DOMAIN_TYPEHASH is calculated during deployment and it's considered immutable. However the parameter chainId can change during a hard-fork of the blockchain, leading to replay attacks between the two child chains.

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/core/GolomTrader.sol#L106

Recommendations

Consider checking chainId against a deployment-fixed value, so in case it's different the appropriate EIP712_DOMAIN_TYPEHASH can be calculated.

#0 - dmvt

2022-10-21T12:06:29Z

Duplicate of #391

Findings Information

🌟 Selected for report: 0xDjango

Also found by: 0x52, 0xsanson, MEP, kenzo, simon135

Labels

bug
duplicate
2 (Med Risk)

Awards

214.0206 USDC - $214.02

External Links

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L99

Vulnerability details

Impact

When delegating voting power to a user, the contract stores a checkpoint with all delegated tokens in an array uint256[] delegatedTokenIds.

When we want to delegate a new tokenId to toTokenId, the contract requires that the array length stays under 500:

require(_delegatedTokenIds.length < 500, 'VVDelegation: Cannot stake more');

This means that it's possible for someone to delegate 500 tokens to a victim and stop further delegations. Notice that there's no floor cost for this attack beside gas costs (since MIN_VOTING_POWER_REQUIRED = 0).

Proof of Concept

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L99

Consider changing MIN_VOTING_POWER_REQUIRED to a relatively high value.

#0 - zeroexdead

2022-08-15T14:15:32Z

Duplicate of #707

Findings Information

🌟 Selected for report: 0x52

Also found by: 0xsanson

Labels

bug
duplicate
2 (Med Risk)
upgraded by judge

Awards

978.6038 USDC - $978.60

External Links

Judge has assessed an item in Issue #907 as High risk. The relevant finding follows:

Lines of code

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L210 https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L242

Vulnerability details

Impact

In VoteEscrowDelegation.sol, function _transferFrom will attempt to remove the delegations to the token transferred.

// remove the delegation
this.removeDelegation(_tokenId);
<!-- However this external call will change `msg.sender` to `address(this)`, reverting the call when checking the owner ```js function removeDelegation(uint256 tokenId) external { require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed'); uint256 nCheckpoints = numCheckpoints[tokenId]; Checkpoint storage checkpoint = checkpoints[tokenId][nCheckpoints - 1]; removeElement(checkpoint.delegatedTokenIds, tokenId); _writeCheckpoint(tokenId, nCheckpoints, checkpoint.delegatedTokenIds); } ``` Also, even if the call was made internal, another revert is possible: -->

if the token doesn't have any checkpoints yet, nCheckpoints - 1 will revert for underflow.

Proof of Concept

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L210 https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L242

<!-- 1) Change `removeDelegation` to `public`, and write `removeDelegation(_tokenId)` instead of `this.removeDelegation(_tokenId)`; 2) -->

Add a if (nCheckpoints == 0) return; in removeDelegation before the possible underflow.

#0 - dmvt

2022-10-13T12:20:27Z

Duplicate of #751

#1 - dmvt

2022-10-24T14:37:44Z

Duplicate of #191, not #751

[L1] Extra msg.value is stuck in contract when fillAsk

When filling a "ask", the filler provides ETH by sending msg.value along the call. This value is checked to be greater or equal than the order value:

// attached ETH value should be greater than total value of one NFT * total number of NFTs + any extra payment to be given
require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm');

However there's no refund for sending a greater value, and the extra quantity is stuck in the contract.

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/core/GolomTrader.sol#L217

Recommendations

Since the order value doesn't change in time, consider using == instead of >= in the requirement before. Otherwise refund the extra value to msg.sender.

[L2] Unsafe ERC721 transferFrom

GolomTrader uses transferFrom to move ERC721 tokens, instead of safeTransferFrom. If a token recipient is a contract, it's possible that the NFT will be lost.

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/core/GolomTrader.sol#L9

Recommendations

Consider using safeTransferFrom unless gas savings is prioritized.

[L3] removeDelegation should also delete delegates

In VoteEscrowDelegation.sol, with function delegate we write to the mapping delegates.

    /// @notice Delegate of the specific token
    mapping(uint256 => uint256) public delegates;
    
    function delegate(uint256 tokenId, uint256 toTokenId) external {
    	...
        delegates[tokenId] = toTokenId;
        ...
    }

There's a function called removeDelegation which removes the checkpoint but doesn't touch delegates.

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/vote-escrow/VoteEscrowDelegation.sol#L210-L216

Recommendations

It would make sense to reset also delegates during removeDelegation.

[L4] ETH transfer using transfer instead of call

In function payEther, ETH transfers are done using transfer. This makes a message with a fixed 2300 gas, it's possible that a contract recipient isn't able to execute a fallback function, and revert the whole transaction.

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/core/GolomTrader.sol#L151-L156

Recommendations

Consider using a low-level call. Re-entrancy shouldn't be an issue given the nonReentrant modifiers.

[L5] chainId constant

In GolomTrader, EIP712_DOMAIN_TYPEHASH is calculated during deployment and it's considered immutable. However the parameter chainId can change during a hard-fork of the blockchain, leading to replay attacks between the two child chains.

https://github.com/code-423n4/2022-07-golom/blob/main/contracts/core/GolomTrader.sol#L106

Recommendations

Consider checking chainId against a deployment-fixed value, so in case it's different the appropriate EIP712_DOMAIN_TYPEHASH can be calculated.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter