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
Rank: 94/179
Findings: 4
Award: $58.21
π Selected for report: 0
π Solo Findings: 0
π Selected for report: GimelSec
Also found by: 0x52, 0xA5DF, 0xSky, 0xsanson, Bahurum, CertoraInc, GalloDaSballo, JohnSmith, Lambda, MEP, Twpony, arcoun, berndartmueller, cryptphi, hansfriese, kenzo, kyteg, panprog, rajatbeladiya, scaraven, simon135, zzzitron
26.7695 USDC - $26.77
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L85 https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/vote-escrow/VoteEscrowDelegation.sol#L101
When the user has owned tokenID, wants to call delegate()
. At first numCheckpoints[toTokenId]
is 0, so nCheckpoints = 0
, but when _writeCheckpoint()
calls checkpoints[toTokenId][nCheckpoints - 1]
, the underflow error will throw.
So the whole delegate()
cannot start to run.
function delegate(uint256 tokenId, uint256 toTokenId) external { uint256 nCheckpoints = numCheckpoints[toTokenId]; if (nCheckpoints > 0) { } else { // nCheckpoints == 0 uint256[] memory array = new uint256[](1); array[0] = tokenId; _writeCheckpoint(toTokenId, nCheckpoints, array); } } function _writeCheckpoint( uint256 toTokenId, uint256 nCheckpoints, uint256[] memory _delegatedTokenIds ) internal { Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; }
checkpoints[toTokenId][nCheckpoints - 1]
will underflow, if nCheckpoints == 0
.
When _writeCheckpoint
is executed for tokenId for the first time, nCheckpoints
will be 0. so delegate()
function can not start to work.
Manual review
Change Checkpoint memory oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1];
to like the following:
Checkpoint memory oldCheckpoint; if (nCheckpoints == 0) { oldCheckpoint = checkpoints[toTokenId][0]; } else { oldCheckpoint = checkpoints[toTokenId][nCheckpoints - 1]; }
#0 - KenzoAgada
2022-08-02T09:05:56Z
Duplicate of #630
26.7695 USDC - $26.77
delegate()
does not set useful limits for tokenId delegation. For example, tokenId
can not be equal to toTokenId
, otherwise every tokenId
can delegate to itself to get votes; after delegation, there is no state change for tokenId
, it has the same capability to go on delegating.
function delegate(uint256 tokenId, uint256 toTokenId) external {}
``
The following is the test example for delegate()
. The result shows if you have two tokenId, you can delegate from one tokenId to the other tokenId, the votes of delegated tokenId will increase to any value you want.
pragma solidity >=0.6.0 <0.9.0; import "forge-std/Test.sol"; import {VoteEscrow} from "../src/golom/vote-escrow/VoteEscrowDelegation.sol"; import {ERC20Mock} from "../src/golom/test/ERC20Mock.sol"; contract VoteEscrowTest is Test { VoteEscrow vescrow; ERC20Mock erc20mock; address bob = mkaddr("bob"); uint256 lock_duation = 3600 * 24 * 7; uint256 minDeposit = 4 * 365 * 86400; function setUp() public { erc20mock = new ERC20Mock(); vescrow = new VoteEscrow(address(erc20mock)); } function testVoteEscrow() public { vm.warp(1641000000); vm.deal(bob, 100 ether); erc20mock.transfer(bob, 10000 ether); vm.startPrank(bob, bob); erc20mock.approve(address(vescrow), 10000 ether); vescrow.create_lock(10000, lock_duation); vescrow.create_lock(10000, lock_duation); vescrow.create_lock(10000, lock_duation); vescrow.create_lock(10000, lock_duation); vm.warp(1641100000); vescrow.deposit_for(1, minDeposit + 1); vm.stopPrank(); vm.roll(3); vm.warp(1641150000); vm.startPrank(bob, bob); vescrow.delegate(1, 1); emit log_named_uint("GetVotes(1):", vescrow.getVotes(1)); vescrow.delegate(1, 1); emit log_named_uint("GetVotes(1):", vescrow.getVotes(1)); vescrow.delegate(1, 1); emit log_named_uint("GetVotes(1):", vescrow.getVotes(1)); vescrow.delegate(1, 3); emit log_named_uint("GetVotes(3):", vescrow.getVotes(3)); vescrow.delegate(1, 3); emit log_named_uint("GetVotes(3):", vescrow.getVotes(3)); vescrow.delegate(1, 3); emit log_named_uint("GetVotes(3):", vescrow.getVotes(3)); vm.stopPrank(); } function mkaddr(string memory name) public returns (address) { address addr = address( uint160(uint256(keccak256(abi.encodePacked(name)))) ); vm.label(addr, name); return addr; } }
foundry
Add the limits in delegate()
, and set the state changeable variable for any tokenId to track the delegation of every tokenId, in order to avoid abuse of delegation.
mapping(uint256=>bool) public delegated; function delegate(uint256 tokenId, uint256 toTokenId) external { require(tokenId != toTokenId, "tokenId can not delegate to itself"); if (delegated[tokenId]==true){ revert("token has been delegated"); } .... delegated[tokenId]=true; }
#0 - KenzoAgada
2022-08-02T12:00:55Z
Duplicate of #169
π Selected for report: TomJ
Also found by: 0x4non, 0x52, 0xDjango, 0xNazgul, 0xf15ers, 0xsanson, 8olidity, Bnke0x0, CertoraInc, Ch_301, Chom, Dravee, GalloDaSballo, GimelSec, JC, Jujic, Kenshin, Kumpa, Lambda, M0ndoHEHE, PaludoX0, RedOneN, Ruhum, Sm4rty, Treasure-Seeker, TrungOre, Twpony, Waze, _Adam, __141345__, apostle0x01, arcoun, benbaessler, bin2chen, brgltd, cccz, cloudjunky, cryptonue, djxploit, ellahi, erictee, hansfriese, i0001, minhquanym, oyc_109, peritoflores, rbserver, reassor, rokinot, rotcivegaf, saian, shenwilly, sseefried
0.1513 USDC - $0.15
fillAsk()
in GolomTrader.sol
, msg.sender
call this function with msg.value
. If every condition is satisfied, NFT will transfer to receiver
, and msg.value
will be distributed. But if the receiver is the contract and does not have code to deal with NFT, msg.sender
will lose the ether, and NFT will be locked in receiver.
fillBid()
and fillCriteriaBid()
also use transferFrom
for NFT transfer. It is suggested to modify like fillAsk()
. But I think the risk of fillAsk()
is quite greater than the risk of fillBid()
and fillCriteriaBid()
. Because the receiver in fillBid()
and fillCriteriaBid()
is o.signer
, not any receiver
. o.signer
generally has knowledge of this order and may know how to handle NFT. But any receiver
may not.
// fillAsk() ERC721(o.collection).transferFrom(o.signer, receiver, o.tokenId); //fillBid() and fillCriteriaBid() ERC721 nftcontract = ERC721(o.collection); nftcontract.transferFrom(msg.sender, o.signer, o.tokenId);
transferFrom
is not a safe function for ERC721, it will not check whether the NFT receiver will support ERC721 or not. If receiver
is a contract address that does not support ERC721, the NFT can be frozen in the contract receiver
.
As per the documentation of EIP-721:
A wallet/broker/auction application MUST implement the wallet interface if it will accept safe transfers.
Ref: https://eips.ethereum.org/EIPS/eip-721
function fillAsk( Order calldata o, uint256 amount, address referrer, Payment calldata p, address receiver ) public payable nonReentrant { .... if (o.isERC721) { require(amount == 1, 'only 1 erc721 at 1 time'); ERC721(o.collection).transferFrom(o.signer, receiver, o.tokenId); } else { ERC1155(o.collection).safeTransferFrom(o.signer, receiver, o.tokenId, amount, ''); } ... }
Manual review
Add safeTransferFrom
interface in interface ERC721, and change the ERC721 transfer function from transferFrom
to safeTransferFrom
.
interface ERC721 { function safeTransferFrom( address from, address to, uint256 tokenId ) external; } function fillAsk( Order calldata o, uint256 amount, address referrer, Payment calldata p, address receiver ) public payable nonReentrant { .... if (o.isERC721) { require(amount == 1, 'only 1 erc721 at 1 time'); ERC721(o.collection).safeTransferFrom(o.signer, receiver, o.tokenId); } ... }
#0 - KenzoAgada
2022-08-03T15:05:42Z
Duplicate of #342
π Selected for report: AuditsAreUS
Also found by: 0xSky, CertoraInc, GimelSec, GiveMeTestEther, Green, Lambda, Ruhum, RustyRabbit, Treasure-Seeker, Twpony, arcoun, bin2chen, cccz, codexploder, cryptonue, dipp, horsefacts, jayphbee, joestakey, minhquanym, obront, peritoflores, rbserver, reassor, rotcivegaf, scaraven, ych18
4.5163 USDC - $4.52
https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L461 https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L217 https://github.com/code-423n4/2022-07-golom/blob/7bbb55fca61e6bae29e57133c1e45806cbb17aa4/contracts/core/GolomTrader.sol#L459
GolomTrader.sol has realized receive()
and fallback()
, the contract can receive ether, but it does not realize transfer
or withdraw
function for Ether. If user sends ether to GolomTrader.sol carelessly or send more ether in fillAsk()
, the user can not get the ether back.
There is no function to realize the transfer of ETHER from the GolomTrader contract to other address. payEther
is an internal
function, can not be called externally.
In fillAsk(), it requires msg.value is more than or equal to the NFT amount and fee. If the user transfers more to GolomTrader.sol, the user can not get the money back.
function fillAsk() public payable nonReentrant { require(msg.value >= o.totalAmt * amount + p.paymentAmt, "mgmtm"); } fallback() external payable {} receive() external payable {}
Manual review
To realize a function to transfer Ether out, like the following transferEther
function, only the governance
can call the function.
function transferEther(uint256 payAmt, address payAddress) external OnlyGovernance { if (payAmt > 0) { payable(payAddress).transfer(payAmt); } }
#0 - KenzoAgada
2022-08-04T02:57:19Z
The mention of fillAsk
is duplicate of #75
The general issue about receiving ether/no withdrawal method will need to be deduped later