I Dare You Info
Your fans crowdfund a dare aimed at you. You opt in, set your floor and your limits, do the thing, and post the proof. The pot releases to your wallet onchain and you keep 97.5%. No proof, everyone gets refunded.
TrustNet Score
The TrustNet Score evaluates crypto projects based on audit results, security, KYC verification, and social media presence. This score offers a quick, transparent view of a project's credibility, helping users make informed decisions in the Web3 space.
Real-Time Threat Detection
Real-time threat detection, powered by Cyvers.io,
is currently not
activated
for this project.
This advanced feature provides continuous monitoring and instant alerts to safeguard your assets from potential security threats. Real-time detection enhances your project's security by proactively identifying and mitigating risks.
For more information, click here.
Security Assessments
Summary and Final Words
No crucial issues found
The contract does not contain issues of high or medium criticality. This means that no known vulnerabilities were found in the source code.
Ownership is not renounced
The owner retains significant control, which could potentially be used to modify key contract parameters.
Contract is upgradeable
The contract uses a proxy pattern or similar mechanism, enabling future upgrades. This can introduce risks if the upgrade mechanism is not securely managed.
Scope of Work
This audit encompasses the evaluation of the files listed below, each verified with a SHA-1 Hash. The team referenced above has provided the necessary files for assessment.
The auditing process consists of the following systematic steps:
- Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
- Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
- Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
- Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
- Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
- Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
- Actionable Recommendations: Provide detailed, specific, and actionable steps to secure and optimize the smart contracts.
A file with a different Hash has been intentionally or otherwise modified after the security review. A different Hash may indicate a changed condition or potential vulnerability that was not within the scope of this review.
Final Words
The following provides a concise summary of the audit report, accompanied by insightful comments from the auditor. This overview captures the key findings and observations, offering valuable context and clarity.
Smart Contract Analysis Statement
Contract Analysis
The IDareYouEscrow contract is an upgradeable escrow for crowdfunded dares. Contributors pledge a fixed stablecoin toward a challenge. A performer posts a deposit in the network currency, performs the challenge, and submits proof. If nobody disputes the proof, the funds go to the performer, minus a capped protocol fee. Disputes are decided by a platform role. Every failure path returns the money through a withdrawal ledger.
The handling of funds is the strongest part of the contract. We ran a set of targeted proof tests, an independent property campaign of 128,000 calls, more than four million guided random calls, and six formal proofs. We found no path that creates, destroys, or misdirects value. Deposit solvency, token solvency, and per-dare contribution accounting all hold. The fee split is exact and loses nothing to rounding. We also rebuilt the deployed contract from the published source and confirmed that it matches byte for byte.
Three areas need attention before the contract holds real value. They are described in the section below.
Ownership Privileges
The project has kept ownership of the contract. It is currently held by a single ordinary wallet. We confirmed this on the live network rather than taking it from the documentation. The same wallet also holds the dispute role and the fee recipient role. One private key therefore controls three separate powers at the same time. The owner has the following privileges:
- Replace the entire contract code at any time. There is no delay, no second approver, and no period in which users can withdraw first. This is the most significant power, because the contract holds both the pledged tokens and the posted deposits.
- Change the platform address, the signing address, and the fee recipient address. The owner can also change the protocol fee, all four time windows, the pause grace period, and the required deposit amount.
- Pause the contract for an unlimited period. This stops new contributions, new claims, proof submission, settlement, and the expiry functions.
- Decide any dispute in either direction. The owner can also cancel any active dare at any time, with no time limit.
- The protocol fee cannot exceed ten percent. This limit is fixed in the code and the owner cannot raise it. We proved this for every possible fee value.
- The funding token is fixed at deployment. No function can change it. The owner therefore cannot replace it with a harmful token or one that charges a transfer fee.
- There is no mint function, no blacklist, no transfer fee, no wallet limit, no transaction limit, and no trading switch. The usual mechanisms for trapping users are absent.
- Ownership transfer has two steps. The current owner nominates a successor, and the successor accepts. The zero address is rejected, so the contract cannot be left without an owner by mistake. A nomination cannot be formally withdrawn once it is made.
Security Features
The contract includes several good security practices:
- All payments out use a withdrawal ledger, and each recipient is handled separately. One frozen or blocked address therefore cannot stop settlement or refunds for anyone else. The team introduced this during its own internal review, and it works as intended.
- Every function that moves value has a reentrancy guard. The guard is stored in a separate storage location that cannot collide with the upgrade mechanism. There are no unbounded loops anywhere, and no code loops over contributors or dares. There is also no token callback risk, because the single funding token is fixed at deployment.
- The external authorization signatures are protected against all standard replay and manipulation techniques. The signing domain is recalculated from the chain identifier and the contract address on every use. A signature therefore cannot be reused on a fork, on another chain, or against another deployment.
- The upgrade process is genuinely controlled at the top level. The storage layout is fixed by a stored snapshot with an automatic comparison. The contract code cannot be set up directly, only through the proxy. We confirmed during this review that the upgrade check accepts a valid upgrade and correctly rejects a deliberately unsafe one.
- All arithmetic is checked. The contract contains no unchecked blocks. Two arithmetic warnings from automated scanning were each proven to be unreachable.
Points for Attention
Three issues should be resolved before launch. None of them allows an outside party to take funds today.
The first concerns the protection around upgrades. Two separate controls are meant to prevent an upgrade from corrupting stored data. We tested both directly by introducing a harmful change on purpose. Neither control detected it. One control records only the outer layout, not the internal field order of the two main data structures. The other compares a candidate against the current code rather than against the version that was deployed. Swapping two fields of the same size inside either structure therefore passes unnoticed. Existing records would then be read in the new order. An outsider cannot trigger this, and it requires an authorized upgrade. It must still be fixed before any upgrade is carried out.
The second concerns the timing rules. Every deadline is stored in each dare when the dare is created. A later configuration change therefore cannot alter a dare that is already running. The pause grace period is the one exception, because it is read from the global settings each time. Lowering it can move a running deadline backward. A performer who is still within the current deadline can become late as a result, and lose their deposit. No compromised key and no bad intent are required. An ordinary reduction of that setting is enough, and no event reports the change. There is a second and separate effect. A pause that happens later can reactivate a dare that has already expired. Fixing the first behavior does not fix the second.
The third concerns the performance deposit. The deposit exists so that a performer who commits and then does not deliver loses it. In practice this can be avoided. Submitting an empty proof is accepted, and it moves the dare into a state where the deposit can no longer be taken. The performer can then dispute their own submission. If the arbiter does not decide in time, the performer receives the deposit back in full. The deposit therefore only penalizes a performer who does nothing at all.
Two further issues of lower severity are also worth addressing. First, any wallet can claim an open dare and set a funding target that can never be reached, which prevents that dare from ever succeeding. Second, a contributor of a single token unit can dispute a submitted proof. If the arbiter does not respond in time, the performer receives nothing, while the contributor recovers their own stake. Neither issue causes a loss of contributor funds. The project had already identified the second one itself.
Outside the contract, the strength of the identity guarantee depends on an external signing service. Its key is currently a single wallet held online. The contract accepts any valid signature by design and cannot check who the service issued it to. That service and its issuing rules should therefore be secured together with the administrative keys.
Note - This Audit report consists of a security analysis of the IDareYouEscrow smart contract. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contract for the IDareYou team. Other contracts associated with the project were not audited by our team. We recommend investors do their own research before investing.
Files and details
Findings and Audit result
high Issues | 1 findings
Acknowledged
#1 high Issue
Unbounded single-key upgrade authority with no timelock
The upgrade check has one condition: the caller must be the admin. There is no timelock, no delay, and no second approver. The contract holds both the pledged tokens and the ETH bonds. Replacing the implementation is therefore the same as handing over custody. A new implementation can move the entire token balance and the entire ETH balance in one transaction. No user has time to exit first. On the live deployment, the admin is an ordinary wallet with no code. The same wallet also holds the platform role and the fee recipient role. One private key therefore controls arbitration, fee collection, and code replacement.
medium Issues | 6 findings
Resolved
#1 medium Issue
Lowering the pause grace period shortens deadlines that are already running
Each lock stores its own copy of every time window when it is created. A later parameter change therefore cannot alter a lock that is already running. The pause grace period is the one exception. It is read from the global configuration each time a deadline is calculated. After any pause, the deadline is the later of two values: the base deadline plus the pause credit, and a floor derived from the grace period. When the floor is the later value, lowering the grace period reduces it. The deadline then moves backward. A receiver who is still within the current deadline can become late in the same block as an unrelated configuration change. Proof submission then fails. Anyone can then trigger the no-show path, which sends the receiver's bond to the treasury. The same cause also closes dispute windows early. No compromised key and no bad intent are required. A routine reduction of the grace period is enough, and no event reports the change.
Resolved
#2 medium Issue
An open dare can be blocked permanently by an unreachable funding target
Any wallet can claim an open dare. The claimant chooses the funding target, not the creator. The only checks are that the target is above zero and that any stretch tiers increase. There is no upper limit. The creator has no control, because dare creation accepts no target and no approval step. An attacker can therefore claim a public dare first and set the target to the maximum possible value. The dare can then never reach the funded state. No other wallet can take over, because a claim is only accepted on a lock that is still unclaimed. Both exit paths return the attacker's bond in full. The attack therefore costs only gas, and it can be repeated each time the creator publishes the dare again. This is more than a normal race for the claim, which the open mode accepts by design. An unreachable target means the dare cannot be performed at all.
Acknowledged
#3 medium Issue
A contributor of one token unit can block the payout at almost no cost
Opening a dispute requires only a contribution above zero. There is no minimum amount, no deposit, and no penalty for a dispute without merit. Contributions are accepted until the proof is submitted. The right to dispute can therefore be bought for one millionth of a dollar. If the platform role does not decide within the arbitration window, anyone can trigger the timeout. The timeout sends the lock to the refund state. The default result of an inactive arbiter therefore favors the challenger, not the receiver. The receiver loses the full amount. The attacker gets their contribution back once the lock is in the refund state, so the attack costs only gas. A creator who changes their mind after seeing the proof can use the same route. The team has recorded this as an accepted design decision for the first version. It is reported here because it is still open and easy to exploit.
Resolved
#4 medium Issue
Upgrade checks cannot detect a reordering inside a stored structure or enum
Two separate controls are meant to protect stored data during an upgrade. Neither one can see a change inside a stored structure. The stored layout snapshot records only the top-level entries. It does not list the members of the configuration structure or the lock structure. The upgrade validator uses the current source as its reference. The test fixture it compares against inherits that same current source. Both sides of the comparison therefore change together whenever the base is edited. As a result, swapping two members of the same size inside either structure passes both checks. Reordering the values of a stored enum has the same effect. This was tested directly. Swapping two address members of the configuration structure left the snapshot identical, and the validator reported three of three checks passing. An authorized but incompatible upgrade would then read existing data in the new order. This can exchange the admin with the signing authority, or a funding target with a pledged total.
Acknowledged
#5 medium Issue
The no-show bond forfeit can be avoided by submitting an empty proof
The bond exists so that a receiver who commits and then does not perform loses the deposit. The expiry function enforces this. That function only accepts a lock in the funded state. Proof submission moves the lock out of that state, and it accepts an empty string without any validation. A receiver who cannot perform can therefore submit an empty proof before the window closes. The forfeit then becomes permanently unreachable. From that point the receiver is in a better position than if they had done nothing. If nobody disputes, the settlement path pays them the full amount. If somebody disputes, the worst likely result is a decision against them. The receiver can also dispute their own submission. The creator always has the right to dispute, and so does anyone with a contribution above zero. Self-funding is not blocked, so the receiver can buy that right for one token unit. If the dispute then times out without a decision, the bond is returned in full and contributions are refunded. The bond therefore only penalizes a receiver who never calls the function at all.
Acknowledged
#6 medium Issue
The performance bond can still be recovered by a receiver who never performed
The remediation closed two of the three routes around the bond forfeit: an empty proof is now rejected, and the receiver may no longer open a dispute on their own submission. The third route, which the team deferred, is that an arbiter timeout returns the bond. The reason given for the deferral was that after the self-dispute block the timeout path can only be reached through a genuine adverse dispute from a third party. That reason does not hold. The self-dispute check compares only the caller with the recorded receiver, and dispute standing is granted to anyone holding a contribution above zero. A receiver who cannot perform therefore sends one unit of the funding token from a second wallet they control, submits any non-empty string as proof, has the second wallet contest it, and waits for the arbiter window to lapse. The bond returns to them in full. The same non-performance without any proof loses the bond to the treasury. Submitting a worthless proof is strictly better than not performing, which is the exact behaviour the bond is meant to prevent. No funds are lost by third parties, because pledges unwind to the funders, but the deterrent is neutralised.
low Issues | 11 findings
Resolved
#1 low Issue
Contributions are recorded at the requested amount, not the amount received
The contribution ledger records the amount the caller requested. It does not record the amount the contract actually received. If the funding token ever transfers less than requested, the escrow records more than it holds. This can happen through a transfer fee, a rebase, or a partial transfer. The escrow would then be short of funds across several locks. This is safe today. The funding token is fixed at setup and cannot be changed, every lock is checked against it, and the token in use has no transfer fee. The remaining concern is that the funding token is itself an upgradeable contract controlled by a third party. The safety of this code therefore depends on a decision made outside it. One related point is worth recording. If the token issuer ever froze the escrow address, every outgoing transfer would fail and the funds would be stuck. The refund and payout paths are separated per address, which correctly limits a freeze of one user to that user only.
Resolved
#2 low Issue
Contributions to a funded lock have no deadline check
The funding deadline is checked in the two states before funding is complete. In the funded state there is no time check at all. A lock stays in the funded state until somebody calls the expiry function. Anyone may call it, but nothing calls it automatically. A lock whose performance window closed months ago therefore still accepts new contributions. A contributor can send money to a dare that can no longer settle, because proof submission already fails at that point. The money can be recovered once anyone triggers the expiry and moves the lock to the refund state. This is therefore a usability and capital efficiency problem, not a loss of funds.
Resolved
#3 low Issue
The receiver bond has neither a minimum nor a maximum
The receiver bond is the only configurable value with no limits. Every other parameter is bounded. The fee has a maximum. All four windows must be between one hour and thirty days. The pause grace period has a maximum of one day. A very large bond blocks all three claim paths, because each one requires the sent amount to match the configured amount exactly. A bond of zero is also accepted. This is the more damaging case. All three bond functions exit early when the amount is zero. The return, the forfeit, and the burn then do nothing. The deterrent disappears, but the rest of the state machine still behaves as if a bond existed. Even a bond above zero is a fixed amount, while the funding target and the total contributed are effectively unlimited. The ratio between the deposit and the amount at risk can therefore approach zero. Note also that changing this value affects locks that already exist but are not yet claimed, because the claim paths read the global value at call time.
Resolved
#4 low Issue
A later pause reactivates a phase that has already expired
Every deadline is recalculated from a single starting point recorded when the lock was created. After any pause, the deadline becomes at least the time of the most recent unpause plus the grace period. The calculation never checks whether the phase had already expired when the pause began. A lock whose performance window closed long ago is therefore reactivated by an unrelated pause. The lock only needs to be unfinished, which happens whenever nobody has called the expiry function. A receiver who was already too late can then submit a proof. The forfeit becomes unreachable, because the expiry function no longer accepts the new state. This was confirmed with a lock five days past its deadline and a pause of one second. This is a separate cause from the shortening issue. Fixing or freezing the grace period does not resolve it. It was confirmed to still occur with the grace period set to zero, caused by the accumulated pause credit alone. The same behavior applies to the funding, settlement, and arbitration windows.
Resolved
#5 low Issue
Setup accepts role and token settings that cannot work
The setup function checks the three role addresses and the funding token only for being above zero. It separately rejects a fee recipient equal to the contract itself. Several settings pass these checks but cannot work. The funding token is never checked for containing code, so an ordinary wallet address is accepted. Every contribution then fails, and the token cannot be changed in this version. The signing authority may be set to a contract address. However, signature recovery only supports a single 65-byte signature. A contract signer such as a multisig can therefore never pass the check, and every identity-based lock becomes permanently unclaimable. This point is important. The usual advice to move privileged roles to a multisig must not be applied to the signing authority until contract signatures are supported. The check against using the contract's own address also applies only to the fee recipient, not to the other roles.
Resolved
#6 low Issue
Deployment values are narrowed before they are validated and can be truncated
The deployment script reads its numeric settings as wide values. It then converts them directly to the narrower types the setup function expects. In this language, an explicit narrowing conversion discards the upper bits without any error. An operator mistake that exceeds the range therefore becomes a different value instead of a failure. A fee entered one above the 16-bit range becomes zero. A window that exceeds the 64-bit range becomes the remainder, which can easily fall inside the accepted limits. The setup function and the post-deployment checks only ever see the already truncated value, so none of the existing limits detect the mistake. The post-deployment checks also compare only part of the configuration. They omit the windows, the grace period, and the bond, so a truncated window would not be found there either.
Pending
#7 low Issue
An emergency pause leaves no time to react after it ends
Deadlines are now expressed in active time: a phase gets its window in seconds during which the contract was not paused. That is the correct core and it removes both defects that were reported, since no configuration change can move a running deadline and an expired phase can no longer be revived. What went with it is the minimum buffer the old grace floor also provided. A receiver who had ten seconds left when the contract was paused still has exactly ten seconds when it resumes, no matter how long the outage lasted. Expiry is permissionless, so eleven seconds after a week-long outage ends anyone can call the no-show path and send that receiver's bond to the treasury. The team weighed this trade and chose the simpler model deliberately, and the configured grace value is still accepted at deployment, which makes the absence of any buffer easy to miss in operation.
Pending
#8 low Issue
The open dare floor is bounded from above but not from below
The new ceiling stops a claimant from setting a funding target that can never be reached, which was the reported problem. The mirror case is not covered. Pledges are accepted while a dare is still open and unclaimed, and a funder may withdraw them for as long as that lasts. A claimant who sets the floor to one token unit passes the ceiling check, and because the pledged total already exceeds that floor the claim moves the dare from open straight to funded inside a single transaction. The funders' withdrawal right disappears in the same transaction, with no block in between to react. The claimant then submits a token proof and, if nobody contests it inside the settle window, collects everything that was pledged. The funds are recoverable while the dispute window is open, so the exposure depends on funders watching, but the floor was supposed to be the thing they did not have to watch. Separately, the high end of the range is still a free denial of service: claiming at exactly the creator's ceiling and letting the funding window lapse kills the dare permanently and returns the claimant's bond in full, so the attempt costs only gas.
Resolved
#9 low Issue
A started admin handover cannot be called off
The handover is two steps, which is right, but only the second step can end it. There is no function to clear a pending transfer, and the start function cannot be used to clear it either because it rejects the zero address. Once an address has been nominated it holds a standing right to take the admin role, and that right never expires. If the nomination was a typo, or the nominated key is later found to be compromised, the only way to invalidate it is to nominate a different address, which simply moves the standing right somewhere else rather than removing it. The admin role carries the upgrade authority, so the address holding this right effectively holds custody of every pledge and every bond.
Resolved
#10 low Issue
The reentrancy guard does not cover the whole state machine
Crediting the measured balance difference is the right correctness fix for a token that charges a transfer fee, and it necessarily means the ledger is written after the token call rather than before. The team accepted that and justified it with the reentrancy guard on the contribution function plus the funding token being fixed and callback-free. The second half of that justification carries the weight; the first half is weaker than stated. Five state-changing entry points carry no guard: both claim functions, the self-originated dare creation, proof submission, and dispute opening. A funding token with a sender callback can therefore reach any of them from inside the window the balance measurement opens. This was confirmed by running a token with a sender hook against the contract: proof submission executes from inside the contribution call, and the pledge is then credited to a lock whose state the contribution function rejects at its own entrance. No profitable path was found, because the caller pays for the tokens they are adding and the state reached is one they could reach anyway, so the accepted assumption holds for the intended token. The point is that it rests entirely on the token, not on the guard. The static analyser reports this as its only high-severity result, up from none before the change.
Pending
#11 low Issue
Two transactions with no downtime slide every live deadline by a full buffer
The buffer is armed by the act of unpausing, not by the length of the pause. Turning the pause on and off in the same block records an unpause at the current timestamp, adds nothing to the pause credit, and grants a fresh buffer to every lock whose deadline falls inside it, while the contract is never observably paused. Repeating that just before each buffer lapses extends those deadlines indefinitely in buffer-sized steps. The effect is one-sided in a way a real pause is not: during a real pause the expiry, settlement, and timeout paths all revert, so nothing moves at all, whereas here the contract stays fully live and only the clocks slide. That lets the holder of the pause switch keep a no-show from being penalised, keep a settlement from completing, or keep an arbiter timeout out of reach, an hour at a time, without any of the visible cost of a pause. It is the same lever in the opposite direction to the deadline that moves backwards, and it belongs to the same single key that also holds the upgrade authority. A related detail with the same root: the initializer writes the unpause timestamp at deployment, so the buffer is armed before any pause has happened. It is inert today only because the shortest permitted window equals the buffer length, which is a coincidence between two unrelated constants rather than a check.
optimization Issues | 7 findings
Acknowledged
#1 optimization Issue
Display data is written to storage on every claim, creation, and proof submission
The stretch tiers, the description, and the proof link are all written to contract storage and never read by any code in the contract. Together they can use up to five storage words per claim, plus one or two per creation and per proof submission. A new storage word costs roughly twenty thousand gas.
Acknowledged
#2 optimization Issue
The lock stores its own identifier unnecessarily
Each lock stores its own identifier. This repeats the key the lock is already stored under. The stored copy is used only to check whether a lock exists, which another field that is already written can do just as well.
Acknowledged
#3 optimization Issue
The lock structure is not packed efficiently
The contributor count and the fee rate each start a new storage slot, although they use only four and two bytes. Grouping them with the twenty-byte address fields would save a slot, which is roughly twenty thousand gas per lock.
Acknowledged
#4 optimization Issue
Loop counters and length reads are not optimized
The two loops that handle stretch tiers read the array length on every iteration. They also use a checked increment for the counter. Neither is necessary, because the loop bound is validated beforehand.
Acknowledged
#5 optimization Issue
The same configuration slots are read several times
The dare creation helper reads six configuration fields across three storage slots. Both claim paths read the pause flag and the required bond amount from separate slots. Each read is a separate storage access, where one read into local variables would be enough.
Acknowledged
#6 optimization Issue
Ticket hashing forces a copy from calldata to memory
Both ticket hashing functions take their argument in memory. Every identity-based claim therefore copies a structure with seven fields out of calldata into memory before hashing it, although the caller already holds it in calldata.
Acknowledged
#7 optimization Issue
Setup function visibility
The setup function is public but never called from inside the contract. It therefore uses the more expensive calling convention with no benefit.
informational Issues | 37 findings
Acknowledged
#1 informational Issue
The token transfer happens before the state is updated
The contribution function transfers the tokens before it updates the contributor ledger, the contributor count, and the total contributed. Every other function that moves value writes the state first. This one is the exception. The reentrancy guard blocks reentry into other guarded functions, but not into unguarded ones. The unguarded functions include both claim paths, dare creation, proof submission, dispute opening, and the admin setters. Each of these was reviewed against the intermediate state. None of them allows a profitable reordering. In particular, the right to dispute is based on the contributor ledger, which is still zero at that moment, so a first-time contributor cannot obtain it. The issue is therefore defensive and not currently exploitable. It only becomes reachable if the fixed funding token ever gains a transfer callback. It is still worth fixing. This is the one place where safety depends on the behavior of an external contract, rather than on the order of operations in this code.
Resolved
#2 informational Issue
The parameter update event reports only four of the ten values written
The platform parameter function writes ten storage fields. Its event reports only four of them. The four time windows, the pause grace period, and the required bond amount all change without any event. This makes the deadline shortening issue harder to detect, because the pause grace period is one of the unreported values. The change that can shorten a running deadline and cost a receiver their bond therefore produces no visible signal. The setup function also emits no event, so the initial configuration cannot be reconstructed from logs. As a result, indexers, dashboards, and users cannot detect changes to the bond amount or to any of the five time windows. Reviewing an incident afterward requires reading raw storage.
Resolved
#3 informational Issue
The bad-faith bond burn sends ETH out with no event
Two related functions dispose of a bond, and each emits its own event. One reports the bond returned to the receiver. The other reports the bond sent to the treasury. The burn function emits nothing. The dispute resolution event does show that a bad-faith decision was made, so the fact of a burn is visible. However, neither the amount destroyed nor the receiver appears anywhere in the logs. The only irreversible path in the contract is therefore also the least visible one. The bond total decreases and ETH leaves the contract with no amount recorded. This makes treasury reconciliation and incident review harder than necessary.
Pending
#4 informational Issue
The bad-faith burn is the only outgoing transfer inside a state change
The contract is designed so that no outgoing transfer happens inside a state change. This was the reason the protocol fee was moved to a withdrawal ledger, so that a frozen or failing fee recipient could not block settlement. The bad-faith bond burn does not follow this rule. It sends ETH to the burn address during dispute resolution, and the transaction fails entirely if the transfer fails. The burn address contains no code on any major chain today, so the transfer succeeds. This is still an assumption about an address the project does not control, on a young network. It is also the only place where an external call can stop a state change. The call also forwards all remaining gas. If code that rejects incoming value ever existed at that address, the bad-faith decision would become permanently unusable. No funds would be lost, because the honest-failure decision remains available, but one documented outcome would not be available.
Pending
#5 informational Issue
The funding window restarts when the dare is claimed
Before a dare is claimed, the funding deadline is measured from the creation time. After it is claimed, it is measured from the claim time. A claim that arrives one second before the original deadline therefore restarts the whole window. The campaign can run for almost twice the configured period. With the current setting of fourteen days, this means up to twenty-eight days against an advertised fourteen. The behavior is not described in the scope or specification documents. Contributors who paid early, expecting a limited period, find their money committed for up to twice as long as stated. One factor reduces the severity. A contributor can always withdraw their own contribution while the lock is not yet funded, so nobody is trapped. They only need to notice and act.
Pending
#6 informational Issue
Pausing does not stop all movement of value
Pausing blocks dare creation, both claim paths, contributions, proof submission, settlement, the expiry functions, the dispute timeout, and dispute opening by anyone other than the platform role. It does not block bond withdrawal, payout withdrawal, contribution refunds, cancellation by the creator, voiding by the platform role, or dispute resolution. Value therefore continues to leave the contract during an emergency. There is a good reason for this. A pause that traps user funds is a serious trust problem, and keeping withdrawals open is the safer default. The consequence should still be stated clearly. If the reason for pausing is that funds are being taken, pausing does not stop the withdrawal of amounts that are already queued. There is also an effect in the other direction. A lock in the funded state has no exit during a pause except through the platform role. Cancellation applies only to the two earlier states, and the performance expiry is itself blocked by the pause.
Resolved
#7 informational Issue
A pending admin transfer cannot be canceled
The admin handover has two steps. The current admin nominates a successor, and the successor accepts. There is no third function to withdraw the nomination. The two obvious workarounds are both blocked. Nominating the zero address is rejected as invalid, and nominating the current admin is rejected as unchanged. The pending address can therefore only be changed to another live address. It can never be cleared. If a handover is started toward a wrong or later compromised address, the nomination cannot be withdrawn. The only workaround is to nominate an address that will never accept. This leaves a misleading value in storage and an event suggesting that a transfer is in progress when it is not.
Pending
#8 informational Issue
Settled locks keep old contributor entries
When a lock settles in favor of the receiver, the total contributed is set to zero. The individual contributor entries and the contributor count are left in place. This causes no problem today, and that was verified, because the settled state is excluded from every refund path. It is recorded because it is a risk for future changes. If a later change allowed the settled state in the refund function, an old contributor amount would be subtracted from a total that is already zero. Depending on how it is written, this would either fail or corrupt the accounting.
Resolved
#9 informational Issue
The comment explaining the pause check on dispute opening is wrong
The comment above the pause check in the dispute opening function states that the settlement deadline is not extended during an active pause. It concludes that checking the deadline during a pause would close the window incorrectly. The code does the opposite. The elapsed pause calculation adds the time since the pause began while the contract is paused. The deadline therefore moves forward during a pause and cannot expire while the contract is paused.
Pending
#10 informational Issue
Display data is stored on chain but never read by the contract
The stretch tiers are validated, written to storage, and then never read by any code in the contract. The same applies to the description, the proof link, the contributor count, and the receiver subject hash. The lock also stores its own identifier, which repeats the key it is already stored under and is used only to check whether a lock exists. All of this is written on paths that users pay for, and it is read only by systems outside the contract.
Resolved
#11 informational Issue
An unused public constant is kept as a health check target
A public constant is kept only so that an external health check has something to call. It lost its original purpose when the identity hashing was removed. The reason is documented, but it remains an unexplained item in the interface that will raise questions for anyone reading the contract for the first time.
Pending
#12 informational Issue
Dare creation is free, requires no authentication, and can name any identity
Creating a dare requires no deposit and no fee. An attacker can therefore increase the lock counter and write storage for the cost of gas alone. No code in the contract loops over locks, so the contract itself cannot be blocked this way. The cost falls on external indexers and on the interface. There is a second point. The targeted dare function accepts any identity value from any caller, with no attestation. Anyone can therefore create a lock naming any person, without that person being involved.
Acknowledged
#13 informational Issue
Funding your own dare is not blocked
The contribution function has no check preventing the receiver or the creator from funding their own dare. A receiver can fund their own campaign to reach the target, submit a proof, and settle. They recover their own money minus the protocol fee. This is not directly profitable, because the fee is a real cost. It does allow false activity and reputation. The claim paths do reject a creator claiming their own dare, so the gap applies only to contributions. The team has recorded this as an accepted position, with prevention handled outside the contract.
Acknowledged
#14 informational Issue
Fee rounding favors the user rather than the protocol
The protocol fee rounds down. Any remainder therefore goes to the receiver rather than to the protocol. The amount is at most one base unit per settlement. This is documented as intentional, and the split was proven to be exact with no remainder lost across a wide range of amounts. It is recorded only because protocol fees usually round in the other direction. A reader comparing this contract to that convention should know the direction was chosen on purpose.
Pending
#15 informational Issue
Open dare claims can be taken by a faster transaction
Open dares are claimed on a first-come basis, with no commit and reveal step. A program watching pending transactions can therefore always claim ahead of a genuine claimant. This follows from the documented design of the open mode. It is recorded separately from the unreachable-target finding, which is the part that turns a normal race into a lasting block.
Pending
#16 informational Issue
The setup function is public but never called internally
The setup function is declared public but is never called from inside the contract. It should be external. A public function that is never called internally still uses the more expensive calling convention.
Pending
#17 informational Issue
The compiler version is not fixed in the source files
All source files accept a range of compiler versions. The build configuration selects one version, and the deployed metadata confirms which version was used, so the deployed contract is unambiguous. The open range still allows a different compiler on any future build. This includes a new deployment or an upgrade prepared by a different person on a different machine.
Pending
#18 informational Issue
The audit documents contain outdated and contradictory statements
Several statements in the supplied documents do not match the live deployment or the current code. The scope document names the replaced proxy and implementation addresses. It also states that all four time windows are one hour. The live deployment uses fourteen days, fourteen days, three days, and seven days, with a ten minute pause grace period. The build verification document reports one hundred and four tests, while the suite now contains two hundred and thirteen. Its closing note states that the upgrade validation passes without doing real work. That is no longer true. This review confirmed that the check runs, accepts the safe upgrade fixture, and correctly rejects the deliberately unsafe one.
Pending
#19 informational Issue
The creator can cancel after a receiver has posted a bond
The creator can cancel throughout the committed state. There is no deadline check and no pause check. A creator can therefore cancel after a receiver has posted a bond and committed to performing. This includes canceling just before the contribution that would have reached the funding target. The receiver's bond is returned, so no funds are lost. The receiver's effort is not protected, and the cancellation can be timed deliberately.
Resolved
#20 informational Issue
The deployed proxy cannot be rebuilt from the repository
The deployed proxy is a small standard forwarder. Its source is not in the repository. The deployment script in the repository builds a different and larger proxy. The implementation is the audited part, and it was verified byte for byte against the source during this review. The proxy constructor cannot be inspected, so it is not possible to prove from source that setup happened in the same transaction as deployment. It can only be inferred. This was not exploited on this deployment. The live admin is the expected deployer and locks have been created normally. The team already discloses this gap.
Pending
#21 informational Issue
An unused dependency version creates confusion about what was compiled
The lock file records a top-level library at version 5.1.0. The import settings do not use it. The version that actually compiles into the bytecode is the 5.6.1 copy inside the upgradeable library. The unused entry serves no purpose. It leads a reviewer to check the wrong dependency for known issues.
Pending
#22 informational Issue
A partly filled storage slot before the upgrade reserve is a trap
The cumulative pause counter uses eight of the thirty-two bytes in its slot. Twenty-four bytes remain free directly before the upgrade reserve. A future upgrade that adds a small variable after it will use that free space rather than a reserve entry. The usual rule of reducing the reserve by the number of slots added would then be wrong. Following that rule would move the reserve and every variable after it.
Pending
#23 informational Issue
The upgrade reserve size differs from the common convention
The upgrade reserve is sized so that the total storage layout uses sixty-seven slots. The common convention would produce fifty. This is not a defect, and the layout is fixed by the stored snapshot. It differs from what a reviewer will expect, and the reason is not recorded anywhere.
Resolved
#24 informational Issue
The stated freeze reference does not exist
The handover message and the freeze document both state that the audited code matches a named release tag. The repository contains no tags at all, and the named reference does not resolve. The build verification document is consistent on this point. It records the tag as recommended but not yet created. The tag was therefore planned, and the handover overstated it. As a result, there is no permanent reference for this review. The default branch changes over time, so a third party checking later cannot prove they are looking at the same code. This was handled by pinning the review to the current default branch commit, and by confirming through hashes that both audited source files match it exactly.
Pending
#25 informational Issue
Exact balance equality is not a correct way to state solvency
Anyone can send the funding token directly to the escrow. The native asset can also be forced into a contract without its agreement. Neither action creates a matching obligation. Any statement that the balance equals the obligations can therefore be made false by a third party at no cost. The escrow can still pay everything it owes. There is also a case a user can cause themselves. A payout owner may name the escrow as the recipient. This sets their ledger entry to zero while the balance stays the same, turning their own claim into unassigned surplus. This was confirmed directly. None of this makes the escrow unable to pay. The tests used in this review already state the property as coverage rather than equality, but the published specification states equality and should be corrected.
Pending
#26 informational Issue
Fee arithmetic multiplies before dividing and can overflow at extreme values
The fee is calculated by multiplying the total contributed by the rate, and then dividing by the denominator, in that order. Above roughly one thousandth of the largest representable value, the multiplication overflows and the transaction fails before the division can reduce the result. This cannot be reached with any realistic supply of a six decimal stablecoin. The exactness of the split was proven across a wide but limited range of amounts. This is therefore a robustness note, not a live defect. It is recorded because the earlier description of the fee property as holding without conditions was broader than what was actually proven.
Resolved
#27 informational Issue
One pause is credited again to every later phase
Every phase recalculates its deadline from the starting point recorded when the lock was created. Time spent paused during an early phase is therefore credited again to the funding, performance, settlement, and arbitration windows that follow. One long pause can add its full duration several times across the lifecycle instead of once. This matches the comments in the code and the broad extension the project has already accepted. It is not explained anywhere a user would see it, and it makes the separate problem of reactivated phases worse.
Pending
#28 informational Issue
The pause grace setting no longer does anything but is still required
Removing the grace floor from the deadline calculation left the setting itself in place. It is still validated against an upper bound, still written to storage, still emitted in the configuration event, and still a required deployment input. The most recent unpause timestamp is likewise still written and never read. Nothing reads either of them. An operator reading the deployment inputs, the configuration event, or the deployment script will reasonably conclude that a grace period is in effect after a pause, and there is none. The dedicated error for an over-long grace value reinforces that impression.
Pending
#29 informational Issue
The version string does not distinguish the remediated build
The lock structure gained a field in the middle, so every field after it moved. The team correctly identified that this makes the remediated build storage-incompatible with the deployed one and set a hard condition that it must go out as a fresh proxy rather than an upgrade. The version accessor still returns the same string as the deployed build. An operator, indexer, or incident responder reading it on chain cannot tell the two apart, which is exactly the distinction the hard deployment condition depends on. The upgrade fixture in the repository does bump its own string, so the mechanism exists and is simply not applied to the production contract.
Pending
#30 informational Issue
The audit documents contradict the code and each other
This was reported before and has grown rather than shrunk. The scope document names a proxy and implementation pair that the freeze document explicitly labels superseded, and lists a deployed configuration that no longer describes anything. It states that the reentrancy guard is used on every value-moving external function, which is not the case for the three functions that receive the bond. The build verification document reports a test count from several revisions ago and still carries the caveat that upgrade validation passes trivially and that storage safety is unverified, which was the specific stale statement the previous report asked to have removed and is now the opposite of the truth. The known-issues document describes pause credit as accumulating from lock creation across every later phase, which the remediation deliberately changed. The invariant specification describes the same superseded behaviour in its pause property and states solvency as an exact balance equality. The remediation documents are accurate and thorough; the older ones around them are not, and the freeze document's blanket statement that the newer documents supersede the older ones where they overlap does not tell a reader which statement to discard.
Pending
#31 informational Issue
The dependency lock covers the library that is not compiled and omits the two that are
The lock file pins two paths. One is the test framework. The other is a top-level OpenZeppelin checkout at version 5.1.0 which the remapping file never points at: the compiled contracts resolve through the nested copy inside the upgradeable package, which is version 5.6.1. Neither the upgradeable package nor the upgrade-tooling package appears in the lock file at all. The previous report asked for the unused pin to be removed; it is still there, and the sharper problem is that the reproducibility guarantee the file is supposed to provide currently applies to none of the code that is actually compiled. In practice the libraries are committed into the repository rather than fetched, so the build does reproduce, but the file states something that is not true and the two sources of truth disagree.
Pending
#32 informational Issue
The trap in the last packed slot before the upgrade reserve is still undocumented
The cumulative paused-seconds counter is an eight-byte value occupying its own slot with twenty-four bytes free, immediately before the upgrade reserve. A future variable of eight bytes or less added directly after it packs into that free space and consumes no new slot. Anyone following the usual convention of reducing the reserve by one for each variable added would shift every reserved slot and corrupt the layout. The reserve size also differs from the common convention without a stated reason. The structure that must never be extended now carries a clear warning comment; these two points do not.
Pending
#33 informational Issue
The storage reorder proof passes on any difference including a stale fixture
The member-aware snapshot and the proof step are a genuine improvement and both were run and confirmed working during this review, including that a swap of two same-width members inside either structure is detected. The proof itself asserts only that each fixture's extracted layout differs from the committed baseline. The fixtures are hand-maintained copies of the real structures. If a field is later added to a real structure and the fixture is not updated, the fixture still differs from the baseline and the proof still reports success, but it is then demonstrating that the extractor notices a missing field rather than that it notices a swap of two same-width members. That is the one thing this gate exists to prove.
Pending
#34 informational Issue
Carried-over items from the previous report that were not addressed
Thirteen items remain in the state the previous report described. None of them was introduced or worsened by the remediation, and the gas items among them were deliberately left out of the frozen slice with that decision recorded. The bad-faith burn still sends the bond out with a direct call inside a state change rather than crediting the burn address in the pull ledger, which is harmless in practice because the destination has no code but is the only remaining outgoing transfer on a path that is not a withdrawal. The funding window still restarts from the claim rather than from the creation, and that is still neither capped nor written down. Which functions keep working during a pause is still not documented anywhere, and value still leaves the contract while paused through the withdrawal and refund paths. There is still no comment at the point where a resolved lock's pledged total is zeroed while the per-contributor entries are left behind. Display-only data is still written to storage and never read. Dare creation is still free, unauthenticated, and able to name any identity. An open dare can still be taken by whoever gets a transaction in first. The setup function is still public rather than external. The compiler version is still a range rather than a fixed version in every source file, which the build settings override but the source itself does not state. The creator can still cancel after a receiver has posted a bond. Solvency is still asserted as an exact balance equality. The fee still multiplies before dividing.
Pending
#35 informational Issue
The funding goal an open dare is funded against appears in no event
The goal is now chosen at creation for open dares, and the creation event carries only the kind and the creator. The goal and the stretch tiers reach the log only through the claim event, which is emitted when somebody claims. Funders can contribute from creation onward, so between creation and the first claim a consumer working from logs alone cannot tell a funder what they are funding towards and has to read the lock from storage to find out. This is a direct consequence of moving the goal earlier; it did not exist while the claimant set the goal, because then the goal and the claim arrived together.
Pending
#36 informational Issue
The pause design document describes a deadline formula the code no longer uses
The document is presented as approved and implemented, and states that the deadline calculation returns the base plus the credit and that the unpause timestamp and the grace value are no longer read by the deadline math. The delivered code reads the unpause timestamp inside that calculation. The document then sets out two options, recommends dropping any post-unpause buffer, and argues against the gated-buffer option on the grounds that it is materially more code and more to red-team. The delivered code implements a gated buffer, and with a weaker gate than the one the document sketched: the document proposed gating on the phase being live when the last pause began, the code gates on the phase being live when it ended, and that difference is the reason a later pause can drop the buffer. The document also still claims the never-shortens property for the old formula, and carries a stale test count. A maintainer reading it would conclude that the deadline calculation touches no mutable state, which is the assumption the current defect breaks.
Pending
#37 informational Issue
The buffer length is a constant with no relation to the configured windows
The buffer is a compile-time hour and the shortest permitted window is also a compile-time hour, chosen independently of each other. The scope document records the deployed configuration with all four windows at that minimum. At those values the buffer is not a cushion on a phase, it is a second copy of the whole phase: a phase with a hundred seconds left when a pause ends comes back with a full hour. The same equality is the only reason the buffer the initializer arms at deployment is inert, since it is exactly as long as the shortest window a freshly created lock can have. Nothing checks the relation, so a later change to either constant, or a shorter window becoming permitted, changes behaviour silently in a part of the code whose stated property is that it never shortens anything.