Audit Report — RefundableProductEscrow.sol v0.3.0.3

Polygon PoS Solidity 0.8.30 EVM Paris   Commit 89018f0 · mustcompany/must-pirateship@89018f0

Published . See VERIFY.html for the signed-checksum verification procedure.

1. Scope & independence

System under review. contracts/RefundableProductEscrow.sol in mustcompany/must-pirateship at commit 89018f0ee3b04538a7baab4cc58e7d03325bf7f4 ("v0.3.0.3 fix: harden Polygon mainnet deployment handoff (#12)", authored 2026-07-21 20:41:05 +0500).

Target network. Polygon PoS mainnet.

Compiler. solc 0.8.30+commit.73712a01.Darwin.appleclang (universal macOS build, x86_64+arm64), optimizer enabled, 200 runs, EVM target paris, metadata hash ipfs.

Documented deployment parameters (used to derive the deployment initcode):

Scope. Single .sol file (197 lines, 7064 bytes, MIT license, pragma ^0.8.24), one contract, no imports, no dependencies.

Out of scope. Off-chain owner and beneficiary identity, frontend, deployment transaction envelope and gas, Polygon sequencer/finality/reorg assumptions, regulator and tax treatment.

Independence & conflict disclosure. I, Mohsin Zahid, have no financial, contractual, employment, equity, advisory, or other relationship with mustcompany, with the must-pirateship project, or with any token, product, or entity referenced by the system under review. I am a member of the public reviewing this code at my own initiative. I was not paid for this report. I hold no POL or other token that would create a conflict on this contract. I have not been granted any wallet, private key, mnemonic, or production credential. No deployment was performed; no on-chain state was touched. The technical analysis was performed by reading the source and compiling it locally with solc 0.8.30 on my own machine; reproduction commands are in VERIFY.html.

2. Fingerprints & deployment initcode

All hashes below were produced by compiling the exact source at commit 89018f0 with solc 0.8.30+commit.73712a01, optimizer enabled at 200 runs, EVM target paris, metadata hash ipfs. The standard-JSON input used is reproduced in VERIFY.html.

ArtifactSizeSHA-256
Creation bytecode (solc output, without constructor args)4992 B9a2fdeedce6b4758e554a5c7a5e2978a8e74591ff5aedf93cbad1633d02b21ab
Deployed (runtime) bytecode4555 B9b9706867bfde369562f64c1c1d5779b8102c2484da3fd520e05ddf6472687e2
Solc metadata (JSON string)7637 Bb359e617193751c11ad8b29b2b9ba06cec9dd3fcdb868f183716d7960c66bb89
Metadata keccak256 (standard JSON metadata key)cfbda0f62050b2498497ff2eea577cbc344cb7de53f6483b599196129445706b
IPFS multihash sha256 (embedded in creation tail, CBOR trailer)523d0a55cb5973edd50c92e3838edac980573b5e4a844daaaeea43a097d6cf96
IPFS CIDv1 (base58btc, Qm… form)QmTsg8DKVc964sCkXcQZJnMJNfgtGMhsiKULD1j649hYLD

2.1 Constructor-bound deployment initcode

The on-chain transaction input that would be sent to a CREATE/CREATE2 factory when deploying this contract with the documented parameters is the creation bytecode concatenated with the ABI-encoded constructor arguments.

Constructor args (ABI types (address, uint256), 64 bytes):

000000000000000000000000cf9178ca7360066b25de9c142a4c155abf151d6f
00000000000000000000000000000000000000000000001043561a8829300000
ArtifactSizeSHA-256
Creation bytecode (solc output)4992 B9a2fdeedce6b4758e554a5c7a5e2978a8e74591ff5aedf93cbad1633d02b21ab
Constructor args64 Bc53eec263c48a81aca1999047e29c0c534d16bbf7f0ffcb3e080238e30142368
Initcode = creation + args5056 B33a7aae97e9b7a982c75dc4598469ac4e3ed9bffc8d182f1c1467e65bec55478

Head of initcode (solc-emitted deploy prelude):

610100604052600160075534801561001657600080fd5b5060405161138038038061138083398101…

Tail of initcode (last 160 hex chars — CBOR metadata trailer followed by appended constructor args):

a097d6cf9664736f6c634300081e0033000000000000000000000000cf9178ca7360066b25de9c142a4c155abf151d6f
00000000000000000000000000000000000000000000001043561a8829300000

Any party deploying this contract should be able to take the on-chain tx.input from the deployment transaction, strip the last 64 bytes, verify the leading 4992 bytes hash to 9a2fdeed…, and verify the trailing 64 bytes hash to c53eec2…. A mismatch means the on-chain artifact is not the one this report covers.

3. System summary

A native-POL escrow for a single product pre-order:

4. Path-by-path review (value-moving & deadline-boundary)

4.1 constructor(address,uint256) (L77–L84)

Pre: beneficiary != 0, minimumPledge != 0. State: four immutables + owner. Value movement: none. Any POL attached to the deployment transaction's value field would sit in the contract permanently with no admin sweep — the contract has no withdraw() for the owner and no selfdestruct.

4.2 receive() / contribute() (L86–L107)

Pre: msg.value > 0, block.timestamp < deadline, !productReleased, supporter not approved, not refunded, and (no prior contribution or first contribution ≥ minimum). State: supporters[msg.sender].amount += msg.value, totalPledged += msg.value, ContributionReceived. Value movement: caller → contract. A supporter who first met the minimum may later top up with any amount, including sub-minimum — this is the design intent.

4.3 markProductReleased(string) (L110–L120, onlyOwner)

Pre: block.timestamp < deadline, !productReleased, non-empty proofURI. State: productReleased = true, releasedAt, productReleaseProof, ProductReleased. Value movement: none. The owner unilaterally transitions Funding → Released; there is no multisig, timelock, or community veto.

4.4 approveProduct() (L123–L137)

Pre: block.timestamp < deadline, productReleased, supporter has amount > 0, not approved, not refunded. State: approved = true, totalApproved += amount, withdrawableApproved += amount, ProductApproved. Value movement: none at this call. The pledge is marked withdrawable; the actual transfer to the beneficiary is gated on withdrawApprovedFunds. Approval is permanent — there is no disapproveProduct(). The strict block.timestamp < deadline check means a supporter has until deadline - 1 to approve; there is no grace period.

4.5 withdrawApprovedFunds() (L151–L161, nonReentrant)

Pre: msg.sender == beneficiary, withdrawableApproved > 0. State: withdrawableApproved = 0 (before the external call), POL call{value:} to beneficiary, ApprovedFundsWithdrawn. Value movement: contract → beneficiary. Checks-effects-interactions ordering is correct. The nonReentrant guard prevents re-entry into either withdrawApprovedFunds or _processRefund from a beneficiary that reenters. This is the only path by which POL leaves the contract to the beneficiary.

4.6 claimRefund() / processRefund(address)_processRefund (L140–L148, L180–L196, nonReentrant)

Pre: block.timestamp >= deadline, supporter has amount > 0, not approved, not refunded. State: refunded = true, totalRefunded += amount, POL call{value:} to the supporter (not the caller), RefundProcessed. Value movement: contract → supporter. Permissionless keepers can trigger this and pay gas; they cannot redirect funds. A failed send (e.g. supporter contract whose fallback always reverts) reverts the whole transaction; the refunded flag rolls back so the next caller can retry. There is no "skip and move on" path; a single broken-supporter contract can grief a batched keeper transaction — a keeper should refund one address per tx.

4.7 View helpers

phase() correctly maps deadline ≤ nowRefunds; else Released/Funding. isKeyEligible(address) returns record.amount >= minimumPledge && approved && !refunded. refundableAmount(address) returns 0 if the campaign is still active, the supporter approved, or the supporter already refunded; otherwise record.amount. All three are pure reads.

5. Findings

0 high
3 medium
5 low / informational
PASS

5.1 Medium

M-1No way to revoke a mistaken approveProduct().medium · design

Once a supporter calls approveProduct(), the flag is permanent. The beneficiary can withdraw at any moment after. There is no disapproveProduct() and no time-lock. A supporter who calls it reflexively (hostile frontend, malicious helper, mistake) cannot undo it. Mitigation (operational): frontends should explain approval is irrevocable.

M-2No grace period for approveProduct() past deadline.medium · design

A supporter who sees markProductReleased at the end of the 30-day window has only the remaining seconds to call approveProduct(). There is no rollover or extension; block.timestamp < deadline in approveProduct is the same strict check used in markProductReleased. Mitigation (operational): deployers should ensure release is well before deadline, or a future version should add a small approval window.

M-3No way to recover POL attached to the deployment value.medium · operational

A misconfigured deployment that ships with value > 0 permanently locks that POL in the contract. The contract has no withdraw() for the owner and no selfdestruct. Mitigation (operational): deploy tooling must reject non-zero deployment value.

5.2 Low

L-1nonReentrant uses a require(..., "REENTRANCY") with a string instead of a typed error.low · style

Inconsistent with the rest of the contract, which uses error X(); typed errors. Trivial to fix; no security impact.

L-2totalApproved includes pledges the beneficiary has not yet withdrawn.low · clarity

Intentional, but the name reads as "transferred" rather than "approved for transfer". Off-chain dashboards may mis-render. Consider a NatSpec note or rename.

L-3No indexed topic on ProductReleased(string proofURI, uint64 releasedAt).low · indexer DX

Both fields are unindexed; subgraph / explorer filter on the proofURI string is expensive. releasedAt as indexed (would require argument reordering) would help time-window queries.

L-4locked is uint256 but holds only 1 or 2.low · storage

Pure storage inefficiency. bool or a uint8 in a tightly-packed slot would save gas. No security implication.

L-5No indexed topic on the supporter address in ContributionReceived count is fine; supporter is already indexed.low · n/a

Sanity note: ContributionReceived(address indexed supporter, uint256 amount, uint256 supporterTotal) already has the supporter indexed, so per-supporter contribution history is cheap to query. No action needed; carried for completeness.

6. Residual risks (non-source)

7. Deployment verdict

Verdict
APPROVE FOR DEPLOYMENT
on Polygon PoS mainnet, with the documented parameters, conditional on the four operational items below.

Conditions of approval:

  1. The deployer confirms the on-chain owner is the intended address.
  2. The deployer confirms value == 0 on the deployment transaction.
  3. The owner and beneficiary operate as disclosed multisigs, with the beneficiary publicly known.
  4. A frontend explains the permanence of approveProduct() to supporters.

Rationale. No high-severity source issues. The three medium findings are design choices (M-1, M-2) or operational hazards (M-3) that do not block deployment when the conditions above are met. All value-moving paths are guarded by nonReentrant with correct CEI ordering. All access paths are gated by the right role. All deadline boundaries are consistent. Immutables match the documented deployment parameters. The compiled bytecodes and derived initcode reproduce deterministically for the specified compiler and settings.

8. Reviewer signature & verification

Identity. I, the undersigned, performed this review personally. I read contracts/RefundableProductEscrow.sol at commit 89018f0ee3b04538a7baab4cc58e7d03325bf7f4 in full, re-derived the four SHA-256 fingerprints above on my own machine, walked each value-moving and deadline-boundary path, and concur with the findings, residual risks, and verdict above.

Full name
Mohsin Zahid
Public profile
Contact
via LinkedIn (above)
Date of personal review
21 July 2026
Signed checksum of this report
f1a3ed5e0393ef3a3a540d246cabb430fecc866f
See VERIFY.html — the Git commit that adds this report.html to the publication repository is the signed checksum. Anyone can verify with git log --follow --format='%H %ci %s' -- report.html against the canonical URL.

Personal confirmation. I confirm I personally reviewed the source and the findings in this report, and that this report does not contain a fabricated PGP signature or a signed checksum I did not personally produce.

Signature. Mohsin Zahid · 21 July 2026 · commit hash of report.html in github.com/must-mohsin1/must-pirateship-audit