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: 10/80
Findings: 3
Award: $2,585.28
🌟 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
The StandardPolicyERC1155
contract always returns 1
for the amount
return value. Thus, when fulfilling an order, the exchange only transfers one token from the seller to the buyer, not the value specified in the order.
The user still pays the full price but receives fewer tokens than expected. This is a direct loss of funds and will occur for every ERC1155 order where amount > 1
.
The user can specify the number of tokens in the order here: https://github.com/code-423n4/2022-10-blur/blob/main/contracts/lib/OrderStructs.sol#L19
The actual number of tokens that is sent when the order is fulfilled is determined using _canMatchOrder()
: https://github.com/code-423n4/2022-10-blur/blob/main/contracts/BlurExchange.sol#L145
The function calls the respective policy, in our case StandardPolicyERC1155
: https://github.com/code-423n4/2022-10-blur/blob/main/contracts/BlurExchange.sol#L422-L430
The policy's functions always return 1
for the amount
return value instead of order.amount
: 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
Thus, in the end, only one token is sent to the buyer no matter what the value of order.amount
was. The value is ignored throughout the whole exchange contract.
none
Check that the amount
value of both orders matches and then return that value instead of 1.
#0 - GalloDaSballo
2022-10-13T22:30:15Z
🌟 Selected for report: Soosh
Also found by: 0x1f8b, 0x4non, 0xc0ffEE, RaymondFam, Ruhum, RustyRabbit, Trust, arcoun, cccz, d3e4, minhtrng, rotcivegaf, saian, sakshamguruji
2437.8089 USDC - $2,437.81
https://github.com/code-423n4/2022-10-blur/blob/main/contracts/ExecutionDelegate.sol#L16 https://github.com/code-423n4/2022-10-blur/blob/main/scripts/deploy.ts#L18 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol#L29 https://github.com/code-423n4/2022-10-blur/blob/main/contracts/ExecutionDelegate.sol#L36 https://github.com/code-423n4/2022-10-blur/blob/main/contracts/ExecutionDelegate.sol#L120
The ExecutionDelegate
contract's owner is set to the deployer's address by default. The owner being a hot wallet brings a big security risk. It's likely that the private key will sit on the machine of one of the devs. That is not as secure as having the owner be a multisig or even a timelock contract.
Here are two recent examples of admin keys being compromised that I remember off the top of my head:
https://roninblockchain.substack.com/p/back-to-building-ronin-security-breach https://www.certik.com/resources/blog/2QRuMEEZAWHx0f16kz43uC-harmony-incident-analysis
The risk of compromised keys is very real. It has nothing to do with smart contracts where the main focus of audits lies so it's ignored most of the time. But, if the ExecutionDelegate owner's keys are compromised, the attacker can approve a new contract that is allowed to access the transfer functions. The exchange's users are supposed to approve their WETH tokens to the contract which the attacker can then all steal.
ExecutionDelegate imports OZ's Ownable contract:
contract ExecutionDelegate is IExecutionDelegate, Ownable {}
The Ownable contract will set the msg.sender
as the owner in its constructor:
constructor() { _transferOwnership(_msgSender()); }
If the key is compromised, the attacker can approve a malicious contract using approveContract()
:
function approveContract(address _contract) onlyOwner external { contracts[_contract] = true; emit ApproveContract(_contract); }
They can then use the contract to call transferERC20()
for each user that has approved any tokens to the contract to steal them:
function transferERC20(address token, address from, address to, uint256 amount) approvedContract external returns (bool) { require(revokedApproval[from] == false, "User has revoked approval"); return IERC20(token).transferFrom(from, to, amount); }
none
There's a hardhat task to transfer ownership to the DAO's address. That should be done for the ExecutionDelegate contract within the deployment task. https://github.com/code-423n4/2022-10-blur/blob/main/scripts/deploy.ts#L183
#0 - GalloDaSballo
2022-10-12T19:46:50Z
I really don't think this finding is falsifiable.
There is no way to verify if this will or will not happen
#1 - blur-io-toad
2022-10-16T17:51:08Z
Not sure if key management qualifies as a vulnerability of the protocol, the ownership will be transferred to a multisig soon after deployment.
#2 - GalloDaSballo
2022-10-28T23:16:57Z
In lack of a substantial risk of rug (already disputed the pause as pause doesn't put user funds at risk), am closing as invalid
#3 - GalloDaSballo
2022-11-16T20:01:35Z
Upgrading as dup of #235
#4 - GalloDaSballo
2022-11-16T20:06:46Z
While key management can be arguably set out of scope, the finding does show how approving a new contract can allow the ExecutionDelegate to steal user funds, for this reason am upgrading as dup of 235
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xSmartContract, Aymen0909, Heuss, Lambda, Pheonix, RaymondFam, ReyAdmirado, Ruhum, Shinchan, Shishigami, __141345__, adriro, ajtra, c3phas, ch0bu, cryptostellar5, d3e4, enckrish, gogo, halden, lucacez, mcwildy, medikko, neko_nyaa, pedr02b2, pfapostol, ret2basic, rvierdiiev, saian, sakman, sakshamguruji
32.6464 USDC - $32.65
Storing a bool
to storage is more expensive than an uint256
because the bool has to be packed to 32 bytes.
For a gas optimized ReentrancyGuard contract, check out the solmate: https://github.com/transmissions11/solmate/blob/main/src/utils/ReentrancyGuard.sol
To approve the contract to access your funds you have to use the ERC20 token's approval system & the ExecutionDelegate's own one. Having that additional approval layer is a waste of gas and doesn't really provide any more protection.
You'd save a SLOAD with every transfer.
The value can't realistaclly overflow because of the loop's condition. Thus you can use unchecked to save the opcodes used for the overflow check.
./contracts/PolicyManager.sol:77: for (uint256 i = 0; i < length; i++) { ./contracts/lib/MerkleVerifier.sol:38: for (uint256 i = 0; i < proof.length; i++) { ./contracts/lib/EIP712.sol:77: for (uint256 i = 0; i < fees.length; i++) { ./contracts/BlurExchange.sol:199: for (uint8 i = 0; i < orders.length; i++) { ./contracts/BlurExchange.sol:476: for (uint8 i = 0; i < fees.length; i++) {
i++
saves the original value in memory and returns that while ++i
just returns the new value. By not saving the original value to memory you save a little gas.
./contracts/PolicyManager.sol:77: for (uint256 i = 0; i < length; i++) { ./contracts/lib/MerkleVerifier.sol:38: for (uint256 i = 0; i < proof.length; i++) { ./contracts/lib/EIP712.sol:77: for (uint256 i = 0; i < fees.length; i++) { ./contracts/BlurExchange.sol:199: for (uint8 i = 0; i < orders.length; i++) { ./contracts/BlurExchange.sol:476: for (uint8 i = 0; i < fees.length; i++) {
Instead of
function incrementNonce() external { nonces[msg.sender] += 1; emit NonceIncremented(msg.sender, nonces[msg.sender]); }
you can do:
function incrementNonce() external { emit NonceIncremented(msg.sender, ++nonces[msg.sender]); }
Same principle as explained in the above issue.
#0 - GalloDaSballo
2022-10-22T23:26:06Z
5k from nonReentrant 100 from rest