Introducing NFT-based escrows - you can deposit assets and trade escrows by selling your ownership NFT! However, I accidentally renounced ownership for my own escrow. Can you help me recover the funds?
10,000 GREY has been deposited into DualAssetEscrow, which is deployed through EscrowFactory. To withdraw GREY and solve the challenge, the caller should be the owner of EscrowFactory NFT with certain escrowId
escrowId is determined by the addresses obtained from immutable arguments
EscrowFactory NFT can only be minted via the deployEscrow() function. However, the hash of arguments used in cloning will be recorded and can not be used again. We have to pass different arguments but the result of _getArgs() should remain the same as the previously deployed DualAssetEscrow to receive an NFT with the same escrowId
functiondeployEscrow(uint256implId,bytesmemoryargs)externalreturns(uint256escrowId,addressescrow){// Get the hash of the (implId, args) pairbytes32paramsHash=keccak256(abi.encodePacked(implId,args));// If an escrow with the same (implId, args) pair exists, revertif(deployedParams[paramsHash])revertAlreadyDeployed();// Mark the (implId, args) pair as deployeddeployedParams[paramsHash]=true;// Grab the implementation contract for the given implIdaddressimpl=escrowImpls[implId];// Clone the implementation contract and initialize it with the given parameters.escrow=impl.clone(abi.encodePacked(address(this),args));IEscrow(escrow).initialize();// Get the ID for the deployed escrowescrowId=IEscrow(escrow).escrowId();// Mint an ERC721 token to represent ownership of the escrow_mint(msg.sender,escrowId);}
We can not simply adding extra bytes to args due to the calldata length check in DualAssetEscrow::initialize(). Although adding extra bytes can make runSize exceed 65535 bytes and deploy a contract with the expected arguments, the transaction will revert with an out of gas error
When the ClonesWithImmutableArgs proxy is called, the immutable arguments and a 2-byte length field will be appended to the calldata of the delegate call to the implementation contract. The argument is read based on the starting offset and its type
/// @notice Reads an immutable arg with type address/// @param argOffset The offset of the arg in the packed data/// @return arg The arg valuefunction_getArgAddress(uint256argOffset)internalpurereturns(addressarg){uint256offset=_getImmutableArgsOffset();// solhint-disable-next-line no-inline-assemblyassembly{arg:=shr(0x60,calldataload(add(offset,argOffset)))}}
Since tokenY is address(0) and the first byte of the length field is unused, the first byte of the length field can be utilized as the last byte of the tokenY, thus reducing the args passed to deployEscrow() by one byte, resulting in a different paramsHash