Platform: Code4rena
Start Date: 05/10/2022
Pot Size: $50,000 USDC
Total HM: 2
Participants: 80
Period: 5 days
Judge: GalloDaSballo
Id: 168
League: ETH
Rank: 26/80
Findings: 2
Award: $165.30
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: dipp
Also found by: 0x4non, 0x52, 0xRobocop, 0xc0ffEE, 8olidity, Ch_301, Jeiwan, Junnon, KIntern_NA, Lambda, M4TZ1P, MiloTruck, Nyx, PaludoX0, Ruhum, RustyRabbit, Soosh, TomJ, Trust, arcoun, aviggiano, bardamu, cryptonue, csanuragjain, d3e4, enckrish, exd0tpy, hansfriese, jayphbee, joestakey, ladboy233, minhquanym, minhtrng, nicobevi, obront, polymorphism, rokinot, romand, rotcivegaf, rvierdiiev, saian, serial-coder, trustindistrust, zzykxx
114.8239 USDC - $114.82
https://github.com/code-423n4/2022-10-blur/blob/main/contracts/matchingPolicies/StandardPolicyERC1155.sol#L33 https://github.com/code-423n4/2022-10-blur/blob/main/contracts/matchingPolicies/StandardPolicyERC1155.sol#L59
A seller, Alice, can sign a sell order with the parameter amount
set to 2
and matchingPolicy
set to StandardPolicyERC1155
. Bob sees the order, think it's a good deal, and decides to buy it.
The expecation from Bob is to receive 2
of Alice's ERC1155
, because that's what he signed. However, he will only receive 1
of Alice's ERC1155
. Bob, in this case, paid double the amount expected per token.
Copy in execution.test.ts
:
describe('POC', function() { it('seller can trick buyers with wrong ERC1155 amount', async () => { const PRICE_PER_TOKEN = 100000; const AMOUNT_TO_TRANSFER = 2; await mockERC1155.mint(alice.address, tokenId, AMOUNT_TO_TRANSFER); /* Alice generates an order to sell 2 ERC1155 tokens at a price of 100000 per token, which makes it 100000*2 total */ sell = generateOrder(alice, { side: Side.Sell, tokenId, amount: 2, price: 100000 * 2, collection: mockERC1155.address, matchingPolicy: matchingPolicies.standardPolicyERC1155.address, }); /* Bob sign a matching order to buy Alice's 2 ERC1155 */ buy = generateOrder(bob, { side: Side.Buy, tokenId, amount: 2, price: 100000 * 2, collection: mockERC1155.address, matchingPolicy: matchingPolicies.standardPolicyERC1155.address, }); /* The orders gets executed */ const sellInput = await sell.pack(); const buyInput = await buy.pack(); const aliceERC1155BalanceBefore = await mockERC1155.balanceOf(alice.address, tokenId); const bobERC1155BalanceBefore = await mockERC1155.balanceOf(bob.address, tokenId); const bobWETHBalanceBefore = await weth.balanceOf(bob.address); await exchange.connect(bob).execute(sellInput, buyInput); const aliceERC1155BalanceAfter = await mockERC1155.balanceOf(alice.address, tokenId); const bobERC1155BalanceAfter = await mockERC1155.balanceOf(bob.address, tokenId); const bobWETHBalanceAfter = await weth.balanceOf(bob.address); /* Bob signed an order to receive 2 ERC1155 but he only receives 1, paying double the expected price per token */ expect(bobWETHBalanceAfter).to.be.equal(bobWETHBalanceBefore.sub(PRICE_PER_TOKEN * AMOUNT_TO_TRANSFER)); expect(bobERC1155BalanceAfter).to.be.equal(bobERC1155BalanceBefore.add(1)); /* Alice receives the payment minus the fees and only get subtracted 1 token */ expect(aliceERC1155BalanceAfter).to.be.equal(aliceERC1155BalanceBefore.sub(1)); }); })
The fix depends on design choices, by the comment in StandardPolicyERC1155.sol
:
@dev Policy for matching orders at a fixed price for a specific ERC1155 tokenId
it seems like the policy is designed to only transfer 1
ERC1155 token at a time, in this case StandardPolicyERC1155.sol
should check that makerAsk/Bid.amount
and takerAsk/Bid.amount
in both orders is equal to 1
, and return false otherwise.
If this is not the case then StandardPolicyERC1155.sol
should use the value of makerAsk/Bid.amount
and takerAsk/Bid.amount
at #L33 and #L59 while also checking that they are equal.
#0 - blur-io-toad
2022-10-16T23:40:08Z
This was an oversight on my part for not putting this contract as out-of-scope. Our marketplace does not handle ERC1155 yet and so we haven't concluded what the matching critieria for those orders will be. This contract was mainly created to test ERC1155 transfers through the rest of the exchange, but shouldn't be deployed initially. When we are prepared to handle ERC1155 orders we will have to develop a new matching policy that determines the amount from the order parameters. Acknowledging that it's incorrect, but won't be making any changes as the contract won't be deployed.
#1 - trust1995
2022-10-28T00:01:25Z
Dup #666
#2 - GalloDaSballo
2022-10-28T00:09:25Z
🌟 Selected for report: 0x4non
Also found by: 0x1f8b, 0xNazgul, 0xSmartContract, Deivitto, IllIllI, Lambda, RaymondFam, Rolezn, RustyRabbit, Trust, arcoun, bin2chen, brgltd, csanuragjain, d3e4, enckrish, exd0tpy, ladboy233, nicobevi, rbserver, rotcivegaf, simon135, zzykxx
50.4817 USDC - $50.48
execute()
seller != buyer is never checkedThis could lead to third parties griefing users by matching their buying and sell orders. This, with the policies available now, is exploitable only in the case someone is trying to both sell and buy an ERC1155 at the same price, while paying fees; which is quite the edge case. However, in the future this could become a serious issue with introduction of more complex policies.
ChainId
is passed as a parameterThe chainId
parameter at BlurExchange.sol#L106 is passed in the constructor instead of being taken from block.chainid
. In addition to this DOMAIN_SEPARATOR
is a static value, but it should be dynamic to account for a potential change of block.chainId
, in case of hardforks.
If not fixed it could lead to replay attacks in case of hardforks or replay attacks in case of an error in setting the chainId
during deployment, if re-deployed on another chain with the same address.
Both buyers and sellers can generate orders with up to a 100% fee. Since the use cases for having such a high fee are not clear consider limiting the maximum possible fee, this could prevent buyers from trying to grief seller with unexpectedly high fees.
.call
instead of .transfer
At BlurExchange.sol#L508 .transfer()
is used to transfer the ETH fees.
transfer()
is limited to 2300 gas, which is barely enough to emit an event. If:
then some orders might not be executable.
Consider using call()
instead, since execute()
is protected from re-rentrancy.
#0 - GalloDaSballo
2022-10-20T00:33:34Z
[L1] In execute() seller != buyer is never checked L
[L2] ChainId is passed as a parameter L
[L3] Orders can be executed with fees up to 100% L
[QA1] Use .call instead of .transfer L
4L