<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Contract Level on Medium]]></title>
        <description><![CDATA[Stories by Contract Level on Medium]]></description>
        <link>https://medium.com/@contractlevel?source=rss-59e7d64f8d75------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*5vEZwdWbVB88xY7f2yM23Q.jpeg</url>
            <title>Stories by Contract Level on Medium</title>
            <link>https://medium.com/@contractlevel?source=rss-59e7d64f8d75------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 25 May 2026 10:07:57 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@contractlevel/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Compliance at the Contract Level: Building with Chainlink ACE]]></title>
            <link>https://medium.com/@contractlevel/compliance-at-the-contract-level-building-with-chainlink-ace-c733964bf21b?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/c733964bf21b</guid>
            <category><![CDATA[compliance]]></category>
            <category><![CDATA[chain-link]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[smart-contracts]]></category>
            <category><![CDATA[identity]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Tue, 19 May 2026 13:43:38 GMT</pubDate>
            <atom:updated>2026-05-19T17:25:40.910Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jCGZ66xGFdVEZzcP8stwpw.jpeg" /></figure><h4><em>This article covers how </em><a href="https://github.com/contractlevel/yield-v2"><em>Yieldcoin v2</em></a><em> integrates </em><a href="https://github.com/smartcontractkit/chainlink-ace"><em>Chainlink ACE</em></a><em>, what the architecture looks like and the non-obvious problems I ran into.</em></h4><p>Yieldcoin v2 is a compliance-ready, crosschain yield optimizer built entirely on the Chainlink stack. ACE handles contract level compliance enforcement: user operations are checked against a KYC provider through a modular policy engine before any vault or token logic executes.</p><h3>How runPolicy Works</h3><p>Users interact with the Yieldcoin v2 system via the <a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/vaults/ParentVault.sol">ParentVault</a>, which inherits <a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/policy-management/src/core/PolicyProtected.sol">PolicyProtected</a> and receives a <a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/policy-management/src/core/PolicyEngine.sol">PolicyEngine</a> in its constructor. Every user-facing function implements the runPolicy modifier:</p><pre>function deposit(uint256 amount) external runPolicy returns (...)<br>function withdraw(uint256 shareBurnAmount) external runPolicy returns (...)<br>function claimShares(uint256 epochNonce) external runPolicy returns (...)<br>function claimUsdc(uint256 epochNonce) external runPolicy returns (...)<br>function cancelDeposit() external runPolicy<br>function cancelWithdraw() external runPolicy</pre><p>The vault’s function bodies encode no compliance rules. Policy logic exists outside the contract entirely, wired at deploy time by attaching policies to each function selector:</p><pre>policyEngine.setExtractors(selectors, address(senderExtractor));<br><br>policyEngine.addPolicy(address(parentVault), selector, address(kycPolicy), senderParameter);<br>policyEngine.addPolicy(address(parentVault), selector, address(terminalAllow), noParameters);</pre><p>The full execution path for a deposit looks like this:</p><pre>user -&gt; ParentVault.deposit(...)<br>     -&gt; runPolicy // modifier fires before function body<br>     -&gt; PolicyEngine<br>     -&gt; SenderExtractor      // extracts msg.sender as &quot;sender&quot;<br>     -&gt; CredentialRegistryIdentityValidatorPolicy  // validates KYC<br>     -&gt; TerminalAllowPolicy   // returns Allowed<br>     -&gt; // vault body executes</pre><p>When an ACE policy contract runs, it can resolve in one of three ways:</p><ul><li><strong>Revert</strong> — Immediately blocks the transaction from executing</li><li><strong>Continue</strong> — The check passed, but the final execution decision is deferred</li><li><strong>Allowed</strong> — The transaction is approved to execute, and any remaining policies in the stack are skipped</li></ul><p>The KYC policy returns <em>Continue</em>, not <em>Allowed</em>. The PolicyEngine is initialized with<em> defaultAllow = false</em>, which means that a policy chain that ends with <em>Continue</em> rejects the transaction, rather than silently permitting.</p><p><a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/modules/policies/TerminalAllowPolicy.sol">TerminalAllowPolicy</a> is the explicit resolution at the end of every Yieldcoin policy chain. It returns <em>Allowed</em>, which is what actually permits execution. <strong>Its terminal position is not optional</strong>: placed earlier, it would allow execution and skip every subsequent check.</p><h3>The Credential Model</h3><p>Offchain, the user completes KYC with a registered provider. Onchain, that provider writes to two registries:</p><ul><li><a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/cross-chain-identity/src/IdentityRegistry.sol">IdentityRegistry</a>: maps <em>address → CCID</em></li><li><a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/cross-chain-identity/src/CredentialRegistry.sol">CredentialRegistry</a>: maps <em>CCID → a credential type</em></li></ul><p>A CCID is a bytes32 identifier created by the identity provider, unique to the user. The credential type is <em>keccak256(“common.kyc”)</em>.</p><p>The credential requirements are configured with the type, both registries and a data validator:</p><pre>sources[0] = ICredentialRequirements.CredentialSourceInput({<br>            credentialTypeId: keccak256(&quot;common.kyc&quot;),<br>            identityRegistry: address(identityRegistry),<br>            credentialRegistry: address(credentialRegistry),<br>            dataValidator: address(0)<br>        });</pre><p>Setting <em>dataValidator</em> to <em>address(0)</em> configures ACE to check credential existence and validity, not metadata. Richer validation such as jurisdiction class or accreditation status can be added later by wiring in a data validator without touching the vault or token contracts.</p><p>The registries are <a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/policy-management/src/core/PolicyProtectedUpgradeable.sol">PolicyProtectedUpgradeable</a> contracts attached to the PolicyEngine. The KYC provider is authorized to write to the registries by the protocol operator, via the <a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/policy-management/src/policies/OnlyAuthorizedSenderPolicy.sol">OnlyAuthorizedSenderPolicy</a>. Adding and removing providers is a configuration change, not a deployment one.</p><h3>Token-Level Compliance</h3><p>The <a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/token/YieldcoinShare.sol">Yieldcoin v2 share token</a> extends ACE’s <a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/tokens/erc-3643/src/ComplianceTokenERC3643.sol">ComplianceTokenERC3643</a>. Policies are attached to token operations at deploy time: <strong>transfer</strong>, <strong>transferFrom</strong>, <strong>batchTransfer</strong>, <strong>approve</strong>, <strong>increaseAllowance</strong>, and <strong>decreaseAllowance</strong>.</p><p>A single address check is not enough for token compliance. I wrote a <a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/modules/extractors/YieldcoinShareKycExtractor.sol">custom extractor</a> that builds an address list per operation, and a <a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/modules/policies/CredentialRegistryAccountListValidatorPolicy.sol">custom policy</a> that checks every address in that list:</p><ul><li><strong>transfer</strong>: checks <em>caller</em> and <em>recipient</em></li><li><strong>transferFrom</strong>: checks <em>caller</em>, <em>from</em>, and <em>to</em></li><li><strong>batchTransfer</strong>: checks <em>caller</em> and <em>every recipient</em></li><li><strong>approve</strong>: checks <em>caller</em> and <em>spender</em></li><li><strong>increaseAllowance</strong>: checks <em>caller</em> and <em>spender</em></li><li><strong>decreaseAllowance</strong>: checks <em>caller</em> only</li></ul><p>That last one is deliberate. If a user has approved a spender and that spender later loses KYC, the user should still be able to reduce or revoke the allowance. Checking the spender on <em>decreaseAllowance</em> would trap users with live allowances to addresses that can no longer interact with the token.</p><h3>Building Custom ACE Components</h3><p>ACE policies do not automatically know which addresses in a function call should be compliance checked. The PolicyEngine receives the target contract, selector, caller, calldata, and context, but each policy needs named parameters in the shape it expects. Extractors are the adapter layer between calldata and policy inputs.</p><p>For Yieldcoin v2 I wrote three custom components: <a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/modules/extractors/SenderExtractor.sol">SenderExtractor</a>, <a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/modules/extractors/YieldcoinShareKycExtractor.sol">YieldcoinShareKycExtractor</a>, and <a href="https://github.com/contractlevel/yield-v2/blob/main/evm/src/modules/policies/CredentialRegistryAccountListValidatorPolicy.sol">CredentialRegistryAccountListValidatorPolicy</a>.</p><p><strong>SenderExtractor</strong></p><p>For every user-facing function on ParentVault, the only address that needs checking is the caller. SenderExtractor exposes <em>payload.sender</em> as a named ACE parameter so the PolicyEngine can map it into the KYC policy:</p><pre>policyEngine.setExtractors(selectors, address(senderExtractor));<br><br>senderParameter[0] = senderExtractor.PARAM_SENDER();<br>policyEngine.addPolicy(<br>    address(parentVault),<br>    selectors[i],<br>    address(kycPolicy),<br>    senderParameter<br>);</pre><p>The address passed to the policy is <em>payload.sender</em> — the external caller, not something decoded from the vault’s own calldata.</p><p><strong>YieldcoinShareKycExtractor</strong></p><p>For the token, compliance subjects are not only the caller. YieldcoinShareKycExtractor decodes the calldata by selector and builds an address[] called <em>kycAccounts</em>. Every address must pass KYC for that operation. If the extractor only surfaced <em>payload.sender</em>, an approved user could transfer to a non-KYC recipient or approve a non-KYC spender.</p><p><strong>CredentialRegistryAccountListValidatorPolicy</strong></p><p>Chainlink’s standard <a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/cross-chain-identity/src/CredentialRegistryIdentityValidatorPolicy.sol">CredentialRegistryIdentityValidatorPolicy</a> validates one address. For the token, where a single call can involve multiple parties, that’s not enough.</p><p>CredentialRegistryAccountListValidatorPolicy accepts <em>kycAccounts = abi.encode(address[])</em> and loops over every address:</p><pre>for each account in kycAccounts:<br>    validate(account, context)<br>    if false -&gt; revert PolicyRejected<br>return Continue</pre><p>The vault uses SenderExtractor with the standard single-account validator. The token uses the YieldcoinShareKycExtractor and list validator.</p><h3>The Access Control Problem Nobody Warns You About</h3><p>I wrote a full <a href="https://github.com/contractlevel/yield-v2/blob/main/docs/ACCESS_CONTROL_MATRIX.md">access control matrix</a> before finalizing roles at the contract level. If a change would alter the authority model, the matrix is updated first. I strongly recommend anyone integrating ACE to do the same.</p><p>The hardest part of integrating ACE was not the KYC logic. It was reconciling OpenZeppelin’s <a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/extensions/AccessControlDefaultAdminRules.sol">AccessControlDefaultAdminRules</a> (role checks at the modifier) with ACE’s <a href="https://github.com/smartcontractkit/chainlink-ace/blob/main/packages/policy-management/src/policies/RoleBasedAccessControlPolicy.sol">RoleBasedAccessControlPolicy</a> (same concept, but running as a policy through the policy engine).</p><p>My solution was a clear primary division: OpenZeppelin roles control vault and module administration; ACE policies control compliance checks and token and registry permissions.</p><p>ACE also governs some administrative token operations like attachPolicyEngine and setCCIPAdmin, a consequence of ComplianceTokenERC3643 inheritance, which prevented some functions from being overridden.</p><p>Not a perfect separation, but a legible one.</p><p>The rules that came out of this:</p><ul><li><strong>DEFAULT_ADMIN_ROLE</strong> grants and revokes roles, with direct operational functions avoided where possible</li><li>ACE policy stacks and configuration are administered through the PolicyEngine</li><li><strong>PolicyEngine.ADMIN_ROLE</strong> wires policy stacks; <strong>PolicyEngine.POLICY_CONFIG_ADMIN_ROLE</strong> configures policy internals</li><li>Compliance operation is not policy administration; <strong>COMPLIANCE_OPERATOR_ROLE</strong> can freeze an address, not rewire the policy stack</li></ul><p>One asymmetry worth noting: AccessControlDefaultAdminRules imposes stricter guardrails on <strong>DEFAULT_ADMIN_ROLE</strong>: two-step transfer, optional timelock, additional transfer constraints. PolicyEngine has none of these. The vault’s default admin is more secure out of the box than the PolicyEngine’s. The protocol operator must compensate for that at the operational level by ensuring PolicyEngine admin roles are held by appropriately hardened multisigs.</p><h3>A Practical Note on Compilation</h3><p>ACE’s CredentialRegistryIdentityValidator requires <em>via_ir = true</em> in <em>foundry.toml</em> due to stack-too-deep errors. Any policy that inherits from it inherits that requirement. Therefore Yieldcoin v2 uses <em>via_ir = true</em>, which meaningfully slows compilation and test runs.</p><h3>A Note on Chainlink ACE’s Licence</h3><p>Chainlink ACE is released under BSL 1.1, which restricts commercial use for a defined period. The pattern is established in DeFi. Uniswap v3 and Aave v3 both launched under BSL 1.1 before transitioning to open licences.</p><p>The practical implication: the commercial operator needs to contact Chainlink for a use grant before launch.</p><p><em>Yieldcoin v2 is finishing its test suite and seeking a launch partner.</em></p><p><em>If you are building in the Chainlink ecosystem and want to discuss the architecture, or if you are a protocol that needs compliance-ready yield infrastructure, I am happy to talk.</em></p><p><em>One-pager available on request.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c733964bf21b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Yieldcoin & Crosschain Race-conditions]]></title>
            <link>https://medium.com/@contractlevel/yieldcoin-crosschain-race-conditions-35c69b74297b?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/35c69b74297b</guid>
            <category><![CDATA[asset-management]]></category>
            <category><![CDATA[smart-contract-security]]></category>
            <category><![CDATA[yield-optimization]]></category>
            <category><![CDATA[chainlink-ccip]]></category>
            <category><![CDATA[crosschain]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Wed, 06 May 2026 10:11:51 GMT</pubDate>
            <atom:updated>2026-05-06T10:16:45.554Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7bxeosrtRAvfH5M8w_gBDw.png" /></figure><h4><em>Crosschain race-conditions have been the biggest obstacle to Yieldcoin being meaningfully production-grade. This article discusses the security rationale behind Yieldcoin’s design shift from supporting concurrent deposits and withdrawals on all chains to single-chain epoch-batching.</em></h4><p>A crosschain race-condition refers to the scenario where state on chain A is required on chain B, but by the time it is used on B, it might be different on A.</p><h3>System Model</h3><p>Yieldcoin is a crosschain yield optimization protocol. Understanding the architecture is necessary context for the race-condition analysis that follows.</p><p><strong><em>Chains</em></strong></p><p>The system operates across two logical roles:</p><ul><li><strong>Parent chain</strong> — holds authoritative protocol state, including the location of the active strategy.</li><li><strong>Active strategy chain</strong> — where protocol-managed funds are deployed into a yield-bearing protocol (Aave V3). May be the same chain as Parent or a remote chain. Changes via rebalance when a materially better yield opportunity exists elsewhere.</li></ul><p><strong><em>State</em></strong></p><p>Two values underpin every share pricing calculation in the system:</p><ul><li><strong>totalShares</strong> — the total supply of Yieldcoin share tokens in circulation. Lives on the Parent chain.</li><li><strong>TVL</strong> — the total value of the underlying asset deployed in the active strategy. Lives on the active strategy chain as a live balance. Never approximated or tracked on Parent (unless the Parent is the active strategy).</li></ul><p><strong><em>Message flow</em></strong> Pricing calculations require both values, so when the active strategy is on a remote chain from the Parent, at least one must cross a message boundary before the calculation can execute. This is the origin of the race-condition described below.</p><h3>The Problem</h3><p><strong><em>Deposit</em></strong></p><p>When a user deposits USDC into Yieldcoin, it is routed to the active yield-generating strategy. `tvlBeforeDeposit` is read from the strategy chain before depositing the user’s USDC. Assuming the active strategy is on a remote chain, either `tvlBeforeDeposit` or `totalShares` must be communicated across chains before `shareMintAmount` can be calculated.</p><pre>shareMintAmount = usdcDepositAmount * totalShares / tvlBeforeDeposit</pre><p><strong><em>Withdraw</em></strong></p><p>When a user submits their Yieldcoin share tokens to redeem USDC, `totalShares` from the Parent chain and `tvlBeforeWithdraw` from the active strategy chain must both be used alongside `shareBurnAmount` to calculate `usdcWithdrawAmount`.</p><pre>usdcWithdrawAmount = shareBurnAmount * tvlBeforeWithdraw / totalShares</pre><p><strong><em>The race window</em></strong></p><p>In both flows, one value is read locally and one must arrive from a remote chain. They can never be read atomically. Between the moment the remote value is captured and the moment the calculation executes, another operation can mutate the other value. This is the race window.</p><p>The window duration is determined by CRE hop latency — the time between a state change settling on the strategy chain and its corresponding write landing on the Parent chain. In practice, approximately 30–60 seconds per hop.</p><p>In the deposit-in-flight withdrawal case — a deposit increases TVL before the corresponding shares are minted, and a concurrent withdrawal executes against the inflated TVL — the withdrawal overpayment is:</p><pre>withdrawal_overpayment_bps = (deposit_in_flight / tvl_before_deposit) * BPS_DENOMINATOR</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pArp5pLJFCvi02aNRyvurA.png" /></figure><p>For other race directions, such as a withdrawal reducing TVL before the corresponding shares are burned, this ratio is a useful approximation. The exact mispricing differs slightly because the calculation may use stale values in different parts of the pricing formula.</p><p>The impact is worst at low TVL. At launch, with a $1M TVL and a single $100k deposit in flight, a concurrent withdrawal can be mispriced by 1,000 bps — the withdrawer receives 10% more USDC than they are entitled to, and the depositor’s capital directly subsidizes that excess.</p><h3>The Attack</h3><p>The window is short but observable. A sophisticated actor (or bot) monitoring CCIP message transit can time a withdrawal to land during a large in-flight deposit’s race window.</p><p>Concrete example: TVL = $10M, totalShares = 10M. A $500k deposit has settled on the strategy chain, increasing Aave TVL to $10.5M, but the corresponding shares have not yet been minted on Parent. An attacker withdrawing 100,000 shares in this window calculates:</p><pre>usdcWithdrawAmount = 100,000 * 10,500,000 / 10,000,000 = $105,000</pre><p>Fair value is $100,000. The attacker extracts $5,000, paying a 10 bps withdrawal fee of $105. Net profit: $4,895 per event. At $10M TVL with large in-flight operations, this is repeatable for any actor already holding shares or able to source them temporarily, and there is no protocol mechanism to detect or prevent it.</p><h3>Mitigations Considered</h3><p><strong><em>Serialization Gate — </em></strong>A global gate prevents concurrent operations for the duration of a CCIP transaction (and CRE write) — approximately 25 minutes per operation. Only one operation runs at a time. The gate opens when the CCIP leg fully settles. This eliminates the race entirely, but makes the protocol effectively unusable at any meaningful user load.</p><p><strong><em>Transaction Nonces / Onchain Queue — </em></strong>Each operation is assigned a nonce on the Parent, with the strategy chain processing operations in strict nonce order, buffering out-of-order arrivals. This was rejected because the strategy chain would have to hold pending operations, and execute them after the correct one, placing strain on costs. This would either involve a ccipReceive tx looping through queued txes, or a CRE workflow serially draining the queue.</p><p><strong><em>Per-operation Size Cap </em></strong>— Cap each deposit and withdrawal as a percentage of TVL. This limits worst-case mispricing per event, but does not eliminate the race. For example, a 1% deposit cap limits the deposit-in-flight withdrawal overpayment to approximately 100 bps of the withdrawal amount. With only a 10 bps withdrawal fee, this remains profitable in the adversarial case. A cap low enough to make the attack fee-negative would need to be below the withdrawal fee rate, which would severely constrain legitimate user flows. This mitigation reduces blast radius but does not solve the underlying race.</p><p><strong><em>Parent-tracked TVL </em></strong>— Maintain a `lastKnownTVL` on the Parent chain, updated on every settlement. Would allow share calculations to read both tvl and totalShares locally and atomically on Parent, eliminating the cross-chain read pair. Rejected because the active strategy’s live balance is the authoritative TVL precisely because yield accrues there continuously. Any Parent-side TVL representation is perpetually stale between updates and contaminates the core value proposition of the protocol.</p><p>None of these fully solved the problem. Each either constrained the race, moved the complexity elsewhere, or introduced additional operational overhead.</p><h3>Final Design: Epoch-Based Batch Pricing</h3><p>The correct solution eliminates the race by construction rather than constraining it. Yieldcoin adopts an epoch-based batching model.</p><p>As described in the <a href="https://www.zealynx.io/glossary/epoch-based-batching">Zealynx security glossary</a>, epoch-based batching is a settlement mechanism that collects deposit and redemption requests over a defined time period, then processes all requests simultaneously at a single price. The pattern is established in RWA protocols and traditional fund administration, where investor orders are batched and processed at daily NAV cutoffs.</p><p>Applied to Yieldcoin: users submit deposit and withdrawal intents on the Parent chain at any time during an open epoch. The underlying asset token (ie USDC) and Yieldcoin shares are escrowed locally. No crosschain activity occurs at intent submission — <em>there is nothing to race on</em>.</p><p>At epoch close, a single CRE workflow reads TVL from the active strategy chain and writes it to Parent. All financial calculations execute in that single Parent transaction:</p><pre>pricePerShare     = tvl * SHARE_PRECISION / totalShares<br>totalWithdrawUSDC = totalShareBurnAmount * pricePerShare / SHARE_PRECISION<br>netFlow           = totalDepositAmount - totalWithdrawUSDC<br>newShares         = totalDepositAmount * SHARE_PRECISION / pricePerShare<br>s_totalShares     = totalShares + newShares - totalShareBurnAmount</pre><p>The first epoch requires bootstrap handling because `totalShares == 0`. In practice, the initial deposit mints against a fixed initial share price, after which later epochs use the standard price-per-share formula.</p><p>Settlement uses a single TVL snapshot and the current Parent-chain `totalShares` in one atomic Parent-chain execution. No user operation can interleave between the price calculation, share minting, share burning, and settlement accounting. The race window does not exist.</p><p>The net flow — the difference between aggregate deposits and aggregate withdrawals — determines whether the underlying asset moves to the strategy chain for deployment into the active yield-generating protocol, or the strategy chain withdraws the asset from the active protocol and returns it to the Parent. In many epochs, deposit and withdrawal volume partially offsets, requiring only the net difference to bridge across chains. This reduces CCIP message count and cost significantly compared to per-operation routing.</p><p><strong><em>Security Properties</em></strong></p><p>The epoch model provides guarantees that:</p><ul><li>No user operation prices against a live crosschain read pair. Settlement uses one TVL snapshot and the current Parent-chain `totalShares` in a single atomic Parent transaction.</li><li>No pricing advantage exists within an epoch. All participants receive identical settlement pricing regardless of when their intent was submitted.</li><li>CRE failure modes are bounded. If the settlement workflow drops, the epoch stalls. Funds are safe. No per-user flows are stuck.</li></ul><p>The tradeoff is latency. Users submit intents and claim after settlement, rather than receiving shares or the underlying asset immediately. This is a standard model for yield products where capital is deployed across settlement cycles — the same broad pattern used by Centrifuge, Maple, and Ondo. For a protocol optimizing for security over UX immediacy, it is the correct choice.</p><h3>Residual Considerations: Epoch Boundary Timing</h3><p>While the epoch model eliminates all intra-epoch timing games and crosschain races by construction, participants can still optimize submission timing across epoch boundaries to minimize opportunity cost. This is standard economic behaviour in any periodic NAV settlement system — not a protocol-level exploit or source of mispricing — but is called out here for completeness.</p><p>Asset deposits and Yieldcoin shares submitted for withdrawals are escrowed on the Parent chain from intent submission until epoch settlement. Escrowed capital earns no yield. Rational actors therefore minimize idle time by submitting large intents as close to epoch close as possible.</p><p>All participants who submitted intents during the epoch — regardless of when — receive shares at the same settlement price, derived from TVL at epoch close. The late depositor’s sole advantage over an early depositor is opportunity cost.</p><h3>Reduced Failure Surface</h3><p>The per-user/operation architecture this new design replaces had a failure mode for every crosschain hop. Each deposit or withdrawal involved up to 4 CRE writes and a CCIP message. A failure at any transition step would have left funds for individual users stuck in an intermediate step, requiring operator identification, diagnosis, and explicit recovery. At meaningful volume, the recovery surface grows linearly with operation count.</p><p>The epoch model collapses this.</p><p><strong><em>User-facing operations produce no crosschain activity </em></strong>— Deposit and withdrawal intents are recorded on Parent. Tokens are escrowed locally. There are no hops to fail, no intermediate states to get stuck in, and no per-user recovery paths to manage. A failed user transaction reverts cleanly on Parent. The user retries. Nothing else is affected.</p><p>Because no user operation crosses a chain boundary, there is no per-user transaction state to track. The previous v2 design required correlating individual deposit and withdrawal operations across chains — matching origin intents to strategy confirmations to share mints. That tracking infrastructure is eliminated entirely. The epoch nonce is the only identifier needed.</p><p><strong><em>Crosschain activity is net flow only </em></strong>— Per-operation routing sent one CCIP message per deposit and one per withdrawal. The epoch model sends at most one CCIP message per epoch — the net difference between aggregate deposits and withdrawals. In epochs where flows offset, no CCIP message is sent at all. Settlement workflows are protocol-level, not user-level. If one drops, the epoch stalls and all users are affected equally — one global stuck state to recover, not one per user.</p><p>In the per-operation model, operators must distinguish between: deposit stuck pre-strategy, deposit stuck post-strategy, withdrawal stuck pre-strategy, withdrawal stuck post-strategy, CCIP message delayed, CCIP message failed, CRE execution dropped at step N of M, and combinations thereof — multiplied by every in-flight user operation.</p><p>In the epoch model, the same failure surfaces exist at the adapter and CCIP level, but they are epoch-scoped rather than user-scoped. A stuck Aave deposit or withdrawal affects the entire epoch, not a single user. Detection is simpler — one watchdog monitors one epoch state machine rather than one per user operation. Recovery targets one epoch at a time rather than an unbounded queue of individual operations.</p><h3>Cost Reduction</h3><p>This article has focused on the security rationale, but the operational cost reduction is substantial and worth stating explicitly.</p><p>In the per-operation model, every deposit and withdrawal involving a remote strategy chain required up to 4 CRE writes and 1 CCIP message. At any meaningful volume — say 500 deposits and 500 withdrawals per day — that is up to 4,000 CRE executions and 1,000 CCIP messages daily, each carrying gas costs on both source and destination chains.</p><p>The epoch model requires at most 2 CRE writes and 1 CCIP message per epoch settlement, regardless of how many users participated. 1,000 users settling in a single epoch costs the same as 10 users settling in that same epoch. The cost structure is flat per epoch, not linear per user.</p><p>CCIP fees in particular scale with message count. Reducing from one message per operation to one message per epoch — or zero, when deposit and withdrawal volume offsets — can represent a reduction of several orders of magnitude at scale. Those savings accrue directly to users through lower fees and to the protocol through lower operational overhead.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=35c69b74297b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing Yieldcoin v2]]></title>
            <link>https://medium.com/@contractlevel/introducing-yieldcoin-v2-8c8e7520b7b3?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/8c8e7520b7b3</guid>
            <category><![CDATA[regulatory-compliance]]></category>
            <category><![CDATA[smart-contracts]]></category>
            <category><![CDATA[chain-link]]></category>
            <category><![CDATA[stable-coin]]></category>
            <category><![CDATA[smart-contract-security]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Fri, 17 Apr 2026 11:54:53 GMT</pubDate>
            <atom:updated>2026-04-24T17:45:50.117Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Cb45FGEYfUaeXGTsTLY7Xw.jpeg" /></figure><h4>The original v1 codebase <a href="https://blog.chain.link/announcing-the-chainlink-chromion-hackathon-winners/#chainlink_grand_prize_($35_000)">won the Chainlink Chromion hackathon grand prize</a> for enabling optimal stable yield. v2 is the hardened evolution of that vision.</h4><h3>Architecture</h3><p>Yieldcoin v1 was originally built with Chainlink Functions and Automation, which were then later replaced with a CRE workflow in place of the original logic. The workflow did some cool things like calculating APY based on onchain state, but exceeded CRE service quotas — particularly the amount of EVM reads per workflow run.</p><p>A few months ago I said to a friend “if I was going to build Yieldcoin again from scratch, I’d start with CRE”. And that’s exactly what I’ve been doing.</p><p>I started by thinking about how much of the infrastructure can actually be done in CRE, and where the contracts would fit in the peripheral. This led to failure modes between workflows.</p><p>v1 relied on optimistically successful transition steps across up to 4+ crosschain transactions in a single execution path. Any single step could fail. Any crosschain tx could fail. User funds could be left stuck in an unreachable transition state.</p><p>v2’s architectural foundation is failure mode recovery at every transition step — a baseline in the infrastructure for cleaning state and recovering funds no matter what the active step is.</p><p>There are only 3 crosschain tx types in v2 — one for each execution path type — Deposit, Withdraw, and Rebalance. v1 had roughly 3 times as many crosschain tx types as that.<br>There is a maximum of only a single crosschain tx executed in each execution path in v2. Similarly, v1 had roughly 3 times as many as that per path. This makes v2 faster and cheaper, without sacrificing functionality or security.</p><p>Each path type has 5 distinct (sub)paths, giving us 15 possible flows for depositing, withdrawing and rebalancing.</p><p>The Yieldcoin v2 architecture has been mapped out as comprehensively as possible over the last month before a single line of code was written. This architecture map includes paths, events, workflows, transition steps, failure modes, contracts, integrations, state space etc.</p><p>For every single transition step, we want to know:</p><ul><li>crosschain state</li><li>where the funds are</li><li>every failure mode</li><li>recovery mechanism for each failure mode (including why, when, and by who/what)</li><li>timeout threshold</li><li>the last step</li><li>the next step</li></ul><p>v2 explicitly tracks individual deposits, withdraws, and rebalances across chains. This is something v1 never did, and is required for v2’s recovery model.</p><p><strong>Recovery modes are the prima materia of Yieldcoin v2.</strong></p><h3>Fees</h3><p>Yieldcoin v2 introduces a fee model designed for sustainability, simplicity, and fairness to users.<br>v1 charged a configurable deposit fee of up to 1%, which created unnecessary admin complexity and was not sustainable at scale.<br>v2 removes that configurability entirely in favour of hardcoded constants, making it simpler to audit.<br>v2 charges two fees:</p><ol><li><strong>A flat fee on deposits and withdrawals</strong>, taken in USDC. This (or the aggregate of) is intended to cover the operational cost of CRE and CCIP (and possibly contribute to protocol revenue). Users pay for what they use. The decision to pass the cost of CCIP to the user via native was rejected in favour of $LINK reserves. The exact value for this flat fee is essentially a placeholder until we know how to charge more accurately for CRE.</li><li><strong>A management fee of approximately 1% of TVL annually</strong>, collected in Yieldcoin at each rebalance, proportional to time elapsed since the previous rebalance. This is the protocol’s primary revenue stream and scales directly with TVL. It is capped at a maximum of 1% of TVL per year regardless of rebalance frequency. Users keep all of their yield — the management fee dilutes their share of TVL modestly and predictably, rather than taking a cut of their returns. At $10M TVL this generates approximately $100k annually for the protocol. The protocol is designed with fees that are transparent, hardcoded, and impossible to change.</li></ol><p>Management fees are preferable to performance fees for both users and protocol security. For users, a performance fee requires the protocol to accurately measure individual yield earned — a complex calculation that becomes unreliable when crosschain vault shares are transferred between addresses, used as collateral in other protocols, or partially redeemed. This complexity introduces measurement risk: errors in yield calculation directly translate to users being overcharged or undercharged. A management fee requires no per-user yield tracking whatsoever. The fee is calculated solely from the total Yieldcoins across chains and time elapsed — two values that are always precisely known onchain.<br>From a security perspective, this means the fee mechanism has a minimal attack surface: there are no yield calculations to manipulate, no per user state to corrupt, and no edge cases around partial positions or token transfers. The simpler the mechanism, the smaller the audit surface, and the harder it is to exploit.</p><p>For comparison: <a href="https://docs.yearn.fi/getting-started/products/yvaults/overview#yvault-fee-structure">Yearn historically took a 2% management fee and 20% performance fee, but in more recently deployed vaults, takes a 10% performance fee.</a></p><p>My only reservation with this model is potentially not generating enough revenue for an attractive-enough bug bounty.</p><h3>Compliance</h3><p>Yieldcoin v2 is being developed specifically to comply with the US gov’s CLARITY Act. Legislation progress is being monitored closely alongside development. The infrastructure will include modular compliance through Chainlink ACE and CCID, allowing it to stay compliant with future regulations without mutating the core infrastructure.<br>Yieldcoin v2’s CCID implementation will be built for full KYC with any integrateable provider.</p><p>The actual Yieldcoin (YIELD) token in v2 (a vault share token for redeeming deposits + yield) will feature the same compliance and pausable controls as the rest of the infrastructure.<br>The Yieldcoin (YIELD) token will comply with Chainlink’s CCT standard, but also potentially be non-transferrable. The token’s logic is being intentionally built for maximum functionality, yet (contradictorily) maximum regulatory compliance.<br>Chainlink provides a compliant token standard for explicitly freezing specific amounts. Integrating this directly, or taking inspiration from it, is a remaining open design question.</p><h3>Legal</h3><p>The v2 codebase will remain fully and explicitly unlicensed during development, has an assignment CLA for external contributors, and clean IP. This deliberate legal structure will enable maximum flexibility and value to any potential investors, launch partners, licensees etc.</p><h3>Final</h3><p>I am finishing a production-grade implementation of the project I started, on my own terms, to my own audit-ready standard.</p><p>A final word of thanks to George (<a href="https://x.com/ygorz01">@ygorz01</a>) and Caducus (<a href="https://x.com/chain_linkd">@chain_linkd</a>), the external contributors to v1. George picked up Certora at an astonishing pace and wrote some truly beautiful CVL. Caducus brought exceptional understanding of Aave contracts and the kind of sharp adversarial thinking every serious project needs. I’m grateful to have worked with and learned from them both, and I look forward to seeing what they build next.</p><p>Yieldcoin v2 is being built independently by Contract Level.<br>Follow the progress: <a href="https://github.com/contractlevel/yield-v2">https://github.com/contractlevel/yield-v2</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8c8e7520b7b3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Commercial Advice for Developers]]></title>
            <link>https://medium.com/@contractlevel/commercial-advice-for-developers-9fed0061c10e?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/9fed0061c10e</guid>
            <category><![CDATA[copyright]]></category>
            <category><![CDATA[licensing]]></category>
            <category><![CDATA[due-diligence]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[commercial-development]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Wed, 15 Apr 2026 09:35:30 GMT</pubDate>
            <atom:updated>2026-04-15T09:35:30.814Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PSINs6iwF9gt_PVFqqP_iA.jpeg" /></figure><h4>The most important lessons they don’t teach you in Cyfrin Updraft</h4><p>Most developers encounter these issues only after they arise. What follows is practical guidance on avoiding common and costly mistakes in commercial development.</p><h3>1. Understand licensing and copyright</h3><p>Your code is your intellectual property by default. The moment you write it, you own it. But that protection is only as strong as your understanding of how licensing works.</p><p>Every file in your repository is making a statement about who can use your work and how. Make that statement deliberately.</p><p><strong>SPDX-License-Identifier: MIT</strong> at the top of your Solidity files is not a neutral default. It means anyone in the world can use, copy, modify, and distribute your code freely. Think carefully before applying it to commercial work.</p><p><strong>UNLICENSED</strong> means the opposite. No one can use your code without your explicit permission. For commercial protocol development, this is a strong default position. It means your work remains yours until you choose otherwise.</p><p>If you are working with contributors, implement a <strong>Contributor License Agreement (CLA)</strong> from the very first commit. A CLA documents that contributors grant you the rights to their contributions. Without one, every contributor to your codebase has a potential claim over their work. This can create complications later if you want to license, sell, or transfer the protocol.</p><p>Copyright and moral rights are also worth understanding. In many jurisdictions, copyright protects your code automatically. Moral rights — where they exist — protect your identity as the author. Their scope varies significantly: in the UK and EU they are relatively broad, while in the US they are limited and largely do not apply to software. Be careful about signing anything that waives moral rights in jurisdictions where they apply. Once waived, that right is gone permanently.</p><p>Finally, understand the difference between <strong>you personally</strong> and <strong>your company</strong> as a contracting party. If your company signs an IP assignment, your company’s contributions may be assigned. But IP you created personally, before any commercial arrangement existed, belongs to you personally. Your company cannot assign what it does not own.</p><p>Know what you own. Know what you give away — never let it happen by accident.</p><h3>2. Do your due diligence on working relationships</h3><p>If someone wants to commercialize your code or invest in your work, approach the opportunity with proper due diligence. Many opportunities are legitimate and well-structured. The goal here is simply to verify that before committing.</p><p><strong>Is their company incorporated?</strong> In many jurisdictions, unincorporated entities cannot legally hold IP, issue equity, or enforce contracts. Checking this takes just a few minutes — review the company registry for their jurisdiction or ask to see an incorporation certificate.</p><p><strong>Do they have prior relevant experience?</strong> A track record of completed, successful projects is different from a track record of initiated ones. Ask specifically what they have built, launched, and seen through to completion.</p><p><strong>What is the source of their funding and how much runway do they have?</strong> Clear, verifiable funding gives confidence that the project can support the planned work. If your payments are contingent on a future raise closing, it’s important to understand that upfront.</p><p><strong>Can they demonstrate liquidity before asking for your commitment?</strong> Reliable payment from the outset is a strong indicator of long-term stability.</p><p><strong>Do they have signed agreements with their other contributors?</strong> If other people on the team are working informally without signed agreements, this may indicate the project is less formally structured than it appears.</p><p><strong>What happens to your work if the company folds, pivots, or is acquired?</strong> This question is often overlooked and almost always matters. Make sure the answer is in writing before you begin.</p><p>The excitement of a compelling vision is real. Do the due diligence regardless. Ask the uncomfortable questions. The right counterparty will engage with them seriously.</p><h3>3. Get your own lawyer</h3><p><strong><em>Your</em></strong> lawyer — not a shared one, and not someone introduced as “our lawyer.” If the other party refers to counsel as “our lawyer,” be aware that their lawyer’s duty is to them, not to you.</p><p>Independent legal counsel isn’t about being difficult or adversarial. It simply means you’re handling the matter professionally. A good lawyer reviewing an agreement before you sign usually costs far less than resolving issues afterward.</p><p>You don’t need a lawyer for every email. But before you sign anything that involves your IP, your equity, or your commercial future, get independent advice.</p><p>A lawyer can also confirm what you are not bound by. Sometimes that clarity is worth more than anything else.</p><h3>4. Understand what you are signing — and what you are not signing</h3><p>Reading a contract is not enough. You need to understand what each clause actually means and what it commits you to.</p><p>A few specific things to look for:</p><p><strong>IP assignment clauses</strong> transfer ownership of your work to the other party. This is often buried in boilerplate. Understand exactly what is being assigned, when, and to whom. Pre-existing IP you created before the agreement should be explicitly carved out.</p><p><strong>Moral rights waivers</strong> — where enforceable — permanently remove your right to be identified as the author of your own work. Treat this clause with the seriousness it deserves, as it can affect how your contributions are credited in the future.</p><p><strong>Limitation of liability and indemnification</strong> are not the same thing. A limitation of liability clause caps your financial exposure if something goes wrong. An indemnification clause means the other party actively defends or compensates you against third-party claims. You want both. Many agreements only offer the first.</p><p><strong>Non-compete clauses</strong> are often written so broadly that they prevent you from working in your own field. Take time to understand the scope, duration, and which entity would actually be able to enforce it.</p><p><strong>Counterpart clauses</strong> state that the agreement is not effective until all parties have signed. If a counterpart clause exists and you have not signed, the agreement is not binding.</p><p><strong>Termination clauses</strong> define how the agreement ends and what happens when it does. Understand the notice requirements, the grounds for immediate termination, and what obligations survive after the engagement ends.</p><p>Finally, confirm exactly who the contracting parties are. Whether you or your company is signing changes your obligations, and the legal status of the counterparty determines whether the agreement is enforceable at all.</p><p>Only sign once you’re clear on what each clause means. And if something is not in the document, it does not exist as a commitment.</p><h3>5. Get a signed formal arrangement before writing any code</h3><p>This is the most important step, but should only be followed after the previous ones. Avoid writing commercial code until you have a signed formal agreement in place. But don’t just sign anything — make sure the arrangement protects your interests and clearly reflects the terms you’ve discussed.</p><p>Do not commit to working relationships publicly without this. Do not involve other people without this.</p><p>Without a signed agreement, informal conduct, commitments, or payments can sometimes be interpreted as implying a contract on terms you never intended.</p><p>The shared excitement of building something meaningful makes it easy to move fast and skip the paperwork. In practice, a clear formal arrangement sets professional expectations and lets both sides focus on the work with greater confidence.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/1*Mrx24hQR6z6WncQjq1yIcA.jpeg" /></figure><h3>Final Note</h3><p>Most issues in commercial development arise from unclear ownership, undefined expectations, or informal arrangements. These issues are usually avoidable with some deliberate structure and clear agreements early on.</p><p><em>Disclaimer: This article is not legal advice. Get your own lawyer.</em></p><ul><li><a href="https://contractlevel.com">Smart Contract Development &amp; Security</a></li><li><a href="https://github.com/contractlevel">Contract Level</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9fed0061c10e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[vLEI Hackathon Proposal]]></title>
            <link>https://medium.com/@contractlevel/vlei-hackathon-proposal-draft-bd6fdab02c43?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/bd6fdab02c43</guid>
            <category><![CDATA[vlei]]></category>
            <category><![CDATA[identity]]></category>
            <category><![CDATA[chain-link]]></category>
            <category><![CDATA[sybil-resistance]]></category>
            <category><![CDATA[gleif]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Mon, 25 Aug 2025 09:23:04 GMT</pubDate>
            <atom:updated>2025-08-27T20:47:49.434Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u9U9JDvJwCRiVCoNsH5VHA.png" /></figure><h4>This article is the proposal for Contract Level Identity: A Decentralized Fuzzy Extractor. Hackathon questions are formatted as titles/subtitles and answers are normal text.</h4><h3>Proposal title</h3><p>Contract Level Identity</p><h3>Short description</h3><p>At its core, Contract Level Identity (CLID) is a Decentralized Fuzzy Extractor, to address the critical need for absolute sybil-resistance by consensus.</p><p>Here’s the current problem with identity: there is no secure and reliable way to cryptographically guarantee that an onchain tx was executed by a unique individual. To put this more in context with GLEIF: there is no guarantee that the private key controlling a credential through KERI is always used by the unique individual authorized to do so.</p><p>To really understand this problem in the simplest of terms, we must understand that identity and identification are *not* the same thing. Identity is the aggregation of all the physical bits of data about a person that makes them uniquely identifiable out of all other humans on the planet. Whereas identification is physically separate to (and derived from) identity, like a passport, SBT, driving license, social media handle, AID.<br>The conflation of these concepts is the cause of identity oracle attacks.</p><p>Fuzzy extractors are a cryptographic technique for deriving a key from “noisy data” like biometrics. Noisy data is data that is not exactly the same each time it is used, but close enough it can reproduce the same key. Examples of biometric data being noisy could be dust on a fingerprint scan, hair being longer/shorter, or increased wrinkles on a face as the person ages.<br>The fuzzy extracted key can then be used as the base private key for identity.<br>This process needs to be decentralized to make it securely reliable.<br>Users are required to scan their biometrics everytime they sign a message.</p><p>Having a system like this as the base identity layer guarantees the prevention of unauthorized credential use, and would provide regulatory compliant institutions with the foundation they need to ensure non-compliant actors are unable to participate in smart contracts on public blockchains.</p><h3>1. Accelerate Mainstream Adoption of Digital Asset Infrastructure:</h3><h4>How vLEI can unlock institutional participation in regulated on-chain finance? Please describe your idea in terms of process, business model, technology design and architecture as well as other relevant features.</h4><p>As development progresses, the exact technical implementation is subject to change.</p><p>The current thesis is as follows: <br>The computation for the core fuzzy extractor application runs via Multiparty Computation (MPC). An external user’s biometric data is provided to the fuzzy extractor MPC as input for enrollment. Reed-Solomon (or similar error correcting) will be applied to the noisy biometric data, and compared against all previous public enrollment helper data stored on IPFS, to check for duplicate enrollment. If the noisy input successfully decodes against any helper data, a duplicate is confirmed and enrollment is rejected. Otherwise, fresh helper data is generated and stored, enabling future duplicate rejection.</p><p>The biometric input is then used in the fuzzy extractor’s generate() function to create a reproducible key, which signs an enrollment message and produces helper data for subsequent key reproductions. The MPC outputs an attestation/signature confirming the signing key was derived from a unique human. Enrollment signatures can be used onchain to register the user with a contract (and pay a fee). Subsequent uses require reproduce(), ensuring the same individual is always present when signing.</p><p>By binding this process to vLEI credentials, financial institutions can anchor legal-entity verification (via vLEI/KERI) to biometric enforcement of “who is authorized to act.” This enables vLEI-based KYB/KYC at both the signer *and* contract level, reduces settlement risk, and ensures that tokenized securities, cross-border stablecoin/CBDC flows, DEX activity, and even custodial/non-custodial operations are always restricted to verified, compliant actors. In this way, CLID provides the missing trust/identity layer for institutional participation in regulated digital asset ecosystems.</p><h3>What is the expected result from using your idea, product and/or service? If you have already achieved a result, please provide details, providing quantitative data wherever possible.</h3><p>The result from this idea for GLEIF will be the prevention of unauthorized credential use through the integration of a reliable and secure identity layer.</p><p>Consider this scenario:<br>The private key used for controlling an AID can be given to (or stolen by) someone who is not authorized to use it. This scenario may be unlikely, but it is still a vulnerability that exists. When a vulnerability exists, it will eventually be exploited. When dealing with smart contracts, Chainlink, immutable ledgers, etc. end-to-end security is essential.<br>A decentralized fuzzy extracted key can NOT be given to someone else, because it can only ever be derived from a unique individual’s biometrics.</p><p>This will illustrate the foundations for “solving” identity in the age of smart contracts and AI. The philosophical implications are huge and this piece of tech is essential for smart contracts to progress to the next plateau of potentiality.</p><p>I have already confirmed that a transient key, fuzzy extracted from biometric data, can reliably control a stable, deterministic AID through KERI. (Very exciting!)</p><h3>Please briefly outline the critical success factors, and steps, required to bring this idea to life, and a reasonable timeline in which this can be achieved. You may set these out in bullet point.</h3><ul><li>fuzzy extract key from noisy data — complete</li><li>control an AID through KERI with fuzzy extracted key — complete</li><li>biometric input for noisy data — complete with basic input, if there is enough time during the hackathon, will look at implementing additional biometrics</li><li>decentralize fuzzy extraction process</li><li>secure handling of fuzzy extractor helper data</li></ul><p>These can very reasonably be achieved within the 4–5 week timeframe of the hackathon development period to demonstrate a functional MVP. Once these core requirements are achieved, the focus will shift to optimizing security, user experience, and possibly implementing “multi-factor fuzzy extraction” — which is when multiple biometric sources plus a password are used for key generation/reproduction.</p><h3>What are the key benefits to the wider community if we were to go ahead to implement your proposed solution?</h3><p>A base layer of identity, or “absolute sybil-resistance by consensus”, that CLID seeks to demonstrate, would enable:</p><ul><li>voting</li><li>fairdrops</li><li>auctions</li><li>public access compliance for smart contracts (institutions would have the assurance a sanctioned or high-risk actor is not participating in the same public DeFi protocols as they are)</li><li>prevention of identity oracle attacks / sybil attacks</li><li>justification of the word “identity” in Chainlink’s CCID</li><li>securely restricted sections in next-generation/metaverse environments such as professional or NSFW settings</li></ul><p>As a bonus, this system will also inadvertently solve the problem of society plunging into chaos from AI deepfakes and agents in the next few years, where noone can distinguish reality in the digital world. The world will need reliable cryptographic guarantees that they are interacting with a unique human, as opposed to a duplicated identity/sybil attacker or bot/AI agent.</p><h3>What other companies or organizations already offer a similar solution?</h3><p>Everest.org is the only thing that comes close. They use a biometric login to access their onchain wallet. They do not use a fuzzy extractor as far as I know, they use a third-party biometrics provider. <a href="https://wallet.everest.org/">https://wallet.everest.org/</a></p><p>Everest previously did a PoC with GLEIF: <a href="https://medium.com/about-everest-updates-and-news/everest-gleif-and-adb-to-streamline-cross-border-transactions-between-companies-80eda13fcd57">https://medium.com/about-everest-updates-and-news/everest-gleif-and-adb-to-streamline-cross-border-transactions-between-companies-80eda13fcd57</a></p><p>It was actually Everest that inspired this research. I previously developed an automated compliance framework for public access smart contracts with Chainlink and Everest: <a href="https://github.com/contractlevel/compliance">https://github.com/contractlevel/compliance</a><br>Unfortunately, Everest’s provision of absolutely sybil-resistant identity data to the Chainlink network was not reliable, due to their identity infrastructure not being sufficiently decentralized.</p><p>Here is an article I wrote on the topic where I first publicly considered the application of fuzzy extractors in this context: <a href="https://medium.com/coinsbench/the-problem-with-contract-level-compliance-f81528e55866">https://medium.com/coinsbench/the-problem-with-contract-level-compliance-f81528e55866</a></p><p>Worldcoin and Humanode also incorporate biometrics into their approaches, but neither achieve absolute sybil-resistance, and both have a history of being exploited because of this.</p><p>I also found this whitepaper on decentralized digital identity which discusses the application of fuzzy extractors: <a href="https://ceur-ws.org/Vol-3550/paper4.pdf">https://ceur-ws.org/Vol-3550/paper4.pdf</a><br>However their approach is also easily exploitable.</p><h3>What are the key competitive advantages that your team and your proposed solution have over your competitors?</h3><p>Everest is the only serious competitor, and they are not sufficiently decentralized and reliable. Everest’s identity infrastructure is also closed source, whereas Contract Level Identity will be open source.</p><p>Worldcoin has a history of being sybil-attacked where users were able to exchange their “world identities” due to their infrastructure not verifying identity for every action taken: <a href="https://www.coindesk.com/policy/2023/05/24/black-market-for-worldcoin-credentials-pops-up-in-china">https://www.coindesk.com/policy/2023/05/24/black-market-for-worldcoin-credentials-pops-up-in-china</a><br><a href="https://cointelegraph.com/news/worldcoin-singapore-investigation-money-laundering-terrorism-financing">https://cointelegraph.com/news/worldcoin-singapore-investigation-money-laundering-terrorism-financing</a><br>Worldcoin has questionable user data handling: <a href="https://x.com/CA_Kenya/status/1686718736940580864">https://x.com/CA_Kenya/status/1686718736940580864</a><br>Worldcoin has impractical user onboarding. They require users to sign up via one of their “orbs” — a physical device for scanning biometrics. It is unreasonable to expect people to travel to a limited location. Users should be able to enroll from their own device and location.</p><p>Humanode is the same thing. They scan a users’ biometrics once to enroll them into the system, but then never again. So you’d use a typical EOA wallet to scan your biometrics, and get approved as a “unique human” in their system. Then all it takes for an attacker to gain an advantage over this system is hire a village from the third world to sign up and then give the attacker the private keys to the EOA associated with their confirmed “uniqueness”. Humanode even goes as far as describing themselves as “not ideal” in their own marketing material: <a href="https://x.com/humanode_io/status/1919462537612976174">https://x.com/humanode_io/status/1919462537612976174</a></p><p>Another project called zkKYC has the exact same approach and problem.</p><p>The whitepaper I linked in the previous answer which discusses fuzzy extractors basically does the same thing. They suggested minting users an NFT based on fuzzy extracted data, which is completely useless because the private key holding the NFT can still be passed around to anyone in the world.</p><h3>What differentiates you with your competitors? What is your unique value proposition?</h3><p>Contract Level Identity’s unique value proposition is open sourcing absolute sybil-resistance by consensus. When I use the phrase “absolute sybil-resistance by consensus”, I am referring to the guaranteed impossibility of one actor in a system posing as more than that.</p><p>Where Everest fails to reliably provide absolutely sybil-resistant identity data to smart contracts, CLID will provide.</p><p>Where Worldcoin and Humanode have enabled sybil-attacks, CLID will prevent them.</p><p>Basically every so-called identity solution is exploitable because it is based around something that isn’t actually identity (except for Everest, which fails because it’s too centralized).</p><h3>Please indicate any key dependencies on technical, governance or regulatory issues about which the community should be aware.</h3><p>The currently identified technical limitations (which can be worked with/around for the context of a producing an MVP for the hackathon) are:</p><p>1. Secure handling of fuzzy extractor helper data. Fuzzy extractors have 2 key functions: generate() and reproduce(). Generate takes in noisy data and outputs a key and helper data. Reproduce takes noisy data and the helper data to output the same key. The helper data will need to be securely handled. The current working approach is for the user to store it safely themselves, and they are expected to provide it when they scan their biometrics to reproduce their key and sign a message. Secure alternatives for an improved user experience are being explored, such as splitting the helper data and storing its shares separately on infrastructure like IPFS. Though this would need careful consideration of the shares retrieval for the correct user.</p><p>2. The actual decentralization of the fuzzy extraction and message signing process. A lot of research time for this proposal was spent experimenting with running the fuzzy extractor logic in a Trusted Execution Environment, which would then output an attestation/signature anyone could verify. The issue with this though is TEEs are a closed box for secure execution, and the app would need to interact with IPFS for reading and storing helper data. The current working approach is to have the logic running via Multiparty Computation (MPC). Traditionally, participating parties in MPC provide input, so the current challenge is enabling an external user to securely provide input without being one of the computation running parties.</p><p>Another challenge will be handling of biometric input such that 2 persons do not reproduce the same key, whilst the noisy distance remains sufficient for a single person to reliably reproduce their own key.</p><p>Multifactor fuzzy extraction for increased security and user control will be a longterm preference.</p><h3>Please indicate any specific dependencies, uncertainties or implementation risks related to GLEIF architecture or software</h3><p>The only current uncertainty is insufficient familiarity with the software, though this is being improved with vLEI training resources repo. I’ve already confirmed that a transient, fuzzy extracted key can successfully control a persistent, stable AID through KERI.<br>The video resources were beneficial and the LLM-compatible md file of the resources was particularly helpful, so thanks for that.</p><h3>Please indicate commercial benefits for your organizations and clients as well as indirect benefits to regulators, infrastructure providers or other stakeholders.</h3><p>Commercial benefits include the accelerated adoption of digital assets and market infrastructure by TradFi institutions, due to a safer public regulatory environment from the (private and user-controlled) identification of public participant uniqueness.<br>Institutional clients would have securer access to tokenized securities, reduced settlement risk, and a superior compliant environment for stablecoin/CBDC flows, because of CLID’s inherent enforcement of arbitrary implementations at the signer level — as opposed to at the credential level.<br>Private bank chain liquidity could be confidently connected to public chains, knowing that none of the users in the public access system are on sanctions lists, North Korean hackers, etc</p><p>Benefits for the average retail/end user would be enabling all of the things Web3 requires identity for but currently cannot efficiently facilitate (voting, fairdrops, reputation, auctions, etc) — actual sybil-resistant smart contract systems.</p><p>An indirect benefit to regulators would be enhanced oversight, without compromising privacy. CLID would provide regulators with cryptographic proof of unique, vLEI-verified participation in public ecosystems.</p><p>Chainlink’s ACE/CCID would benefit from having a base layer of identity via CLID, and would no longer be vulnerable to identity oracle attacks.</p><p>CLID could be monetized by requiring a registration fee, or fees for subsequent reproduce() signatures.</p><p>Private key compromises is one of the single biggest attack vectors in blockchain. CLID would solve that because biometric keys cannot be compromised (with a high enough multifactor fuzzy extraction threshold).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*jNl60T1saUIr3YoGr4UZyg.jpeg" /></figure><p><a href="https://github.com/contractlevel/identity"><em>Contract Level Identity (CLID)</em></a><em> is a Decentralized Fuzzy Extractor currently being researched and proposed as a solution for the </em><a href="https://www.gleif.org/en/newsroom/events/gleif-vlei-hackathon-2025"><em>vLEI Hackathon</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bd6fdab02c43" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Dev notes: YieldCoin/Chromion hackathon]]></title>
            <link>https://medium.com/@contractlevel/dev-notes-yieldcoin-chromion-hackathon-26685753741c?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/26685753741c</guid>
            <category><![CDATA[invariants]]></category>
            <category><![CDATA[formal-verification]]></category>
            <category><![CDATA[chain-link]]></category>
            <category><![CDATA[hackathons]]></category>
            <category><![CDATA[debugging]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Thu, 03 Jul 2025 19:29:49 GMT</pubDate>
            <atom:updated>2025-07-03T19:29:49.591Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gs7ULtZwNmnOiD6uEntj8A.png" /></figure><h4><em>This is not meant to be a polished article. These are the (mostly) unedited notes taken during development of YieldCoin for the Chainlink Chromion hackathon. These notes are messy, disjointed, often </em>frustrated, <em>and occasionally incoherent, but relatively linear.</em></h4><h3>Get interest rates</h3><p>The current goal is to efficiently find the pool with the highest interest rate for USDC deposits out of Aave and Compound across Eth, Base, Arb, Opt, Polygon, maybe Avalanche.</p><p>The current approach to “getting” the chain and pool with the highest APY is to use Chainlink Functions to read either onchain data or an offchain API. Let’s try reading onchain data with CLF first. We’ll probably want to use ethers.js.</p><p>Compound v3/Comet has this getSupplyRate() function - <a href="https://docs.compound.finance/interest-rates/#get-supply-rate">https://docs.compound.finance/interest-rates/#get-supply-rate</a></p><p>Aave Getter Function: The getReserveData function, from Aave’s V3 Pool contract, returns a ReserveData struct containing the currentLiquidityRate, which is the supply interest rate for USDC pools (in ray, 10^27).</p><p><a href="https://aave.com/docs/developers/smart-contracts/pool#view-methods-getreservedata">https://aave.com/docs/developers/smart-contracts/pool#view-methods-getreservedata</a></p><h3>Contract Modules</h3><p>This project is being built non-linearly. Let’s try and separate as much stuff into different inheritable contracts as possible (ie crosschain, functions, deposits/withdrawals). There will inevitably be crossover with some of this stuff.</p><p>Let’s start with Chainlink Functions. We’ll need the FunctionsClient and FunctionsRequest. Inherit FunctionsClient and set functionsRouter in constructor.</p><p>We’re going to use time based Automation to trigger the Functions call at every interval. Time based Automation is easy mode, we don’t even need performUpkeep, we can just specify any function we want to be called by the Upkeep address. It’s worth noting time based Automation isn’t called by a “Forwarder” address, but the “Upkeep” address. <a href="https://docs.chain.link/chainlink-automation/guides/forwarder#finding-the-forwarder-address">https://docs.chain.link/chainlink-automation/guides/forwarder#finding-the-forwarder-address</a></p><p>I’m not sure how realistic separating bits into different module contracts will be, given how heavily intertwined the various services will be. Let’s just continue with CLF and CLA in the same contract for now with nothing else.</p><h3>Contract Modules 2</h3><p>Going back to the CLF/CLA module, the _fulfillRequest is going to need to update state for the new Strategy. The s_strategy needs to contain the chainSelector (ie Arb or Eth) and the protocol (ie Aave or Compound).</p><p>If the new strategy is on a different chain (or same chain, but different protocol), then we need to send a message to the currentStrategy.chain, telling it where to move funds. If the new strategy is the same as the current, we don’t need to do anything, except maybe emit an event.</p><p>What do we need on every single YieldPeer (ie parent AND child yield contracts - the contracts on each chain that handle deposits, withdraws and rebalances)?? (The Parent is the only one with the CLF/CLA logic)</p><p>Every YieldPeer requires:</p><ul><li>deposit(uint256 amount) function for depositing USDC</li><li>withdraw(uint256 amount) function for withdrawing USDC</li><li>immutable i_aavePool</li><li>immutable i_comet (Compound Pool)</li><li>immutable i_usdc</li><li>immutable i_thisChainSelector - <em>does every peer really need this? or just the parent??</em></li><li>CCIP compatible</li><li>CCIPReceiver inherit</li><li>IRouterClient</li><li>Client</li><li>immutable i_link (each one needs to be funded with LINK too)</li></ul><p>Does every child need certain state updates? Hopefully not, [REDACTED]</p><p>If someone deposits on a child peer, their address along with deposit amount will need to be sent to the parent? The LPCCT can be minted to them on the child chain, but all state should really be consolidated on the parent…</p><p>Does every YieldPeer need to know where the current strategy is? so that if someone deposits on the child chain where the current strategy is, it knows to deposit there?</p><p>If the LPCCT token is ERC677, then users could just transferAndCall it to the system to initiate withdrawals - instead of having to approve first. They could also transferAndCall it to the LPCCTPool to move across chains, including the chainSelector in the bytes data.</p><p>Let’s call the token SHARE or i_share for simplicity now, since it’s not exactly, strictly an LP token. Well it kind of is, but whatever.</p><h3>Share Token</h3><p>So the SHARE token will be a burnable and mintable ERC677, and it will be a CCT. We’re using Chainlink’s BurnMintERC677 contract which is extremely convenient because it comes with all the basic functionality we need. Thank you, Chainlink!</p><p>Let’s think about the deployment and initial setter flow.</p><ol><li>Deploy SHARE</li><li>Deploy SharePool, passing share</li><li>Deploy YieldPeer, passing share</li><li>share.owner() grants burn and mint roles to SharePool and YieldPeer</li></ol><p>Ok that wasn’t so complicated. This should be done in a script that’s run on each chain. It should also include setting allowed chain selectors and peers for CCIP.</p><pre>// mint tokens to msg.sender based on amount and total liquidity<br>// do we want to update state for every yield peer? we&#39;d have to do a for loop for ccipSend<br>// that&#39;d be expensive<br>// the alternative is send a message to the parent contract with the amount to mint<br>// and get a message back which would be slow</pre><p>This is the first significant development issue of this project. Due to CCIP, the first option we have is expensive operations across all peer contracts (ie updating state across ALL peers everytime someone deposits and withdraws). This would be so expensive, it’s definitely not worth it.</p><p>The second option is sending a message to parent chain to fetch state, and return it to deposit/withdrawal chain. This means users would have to wait 45 mins to get their SHARE tokens or redeem their USDC. I think this option is preferable, purely because [REDACTED]</p><p>So… someone makes a deposit or withdrawal and they have to wait for the bidirectional crosschain tx to get their respective tokens.</p><h3>Contract Modules 3</h3><p>YieldPeer is abstract. We inherit it in the ParentPeer and ChildPeer contracts. ParentPeer is where CLF and CLA happen. It’s also where the state of the entire crosschain system is tracked. ChildPeer is where messages get sent to ParentPeer querying and updating state. The purpose of life is to [REDACTED]. [REDACTED] modular smart contract development entertaining? I think so. [REDACTED] mission statement. Should we be storing the parent chain selector as immutable in ChildPeer? I’m not sure yet, but the answer will become clear as development progresses. Let’s update state in the ParentPeer first. We can use modular helper functions for transferring from in the abstract YieldPeer.</p><p>ParentPeer::deposit() now includes _revertIfZeroAmount(amountToDeposit) (YieldPeer helper), _updateDepositState(msg.sender, amountToDeposit) (ParentPeer, might change this to just _updateState and use a flag to indicate deposit or withdrawal later), and _transferUsdcFromMsgSender(amountToDeposit) (another YieldPeer helper).</p><h3>State tracking USDC deposits and SHARE</h3><p>Alright let’s think about this.</p><ul><li>users deposit USDC and mint SHARE in return</li><li>users withdraw USDC and burn SHARE in return</li><li>SHARE is a CCT</li></ul><p>If we are tracking user deposits in the ParentPeer, then this number will differ from the total USDC accumulated in the system because of interest accumulated. We’d track it in some state var like s_totalUsdcDeposits</p><p>Are we gonna need another state var to track total crosschain supply for SHARE? If we are burning and minting across chains, then the totalSupply won’t be accurate.</p><p>If SHAREs represent ownership of protocol funds, then we probably don’t need a mapping for user ⇒ deposit because we want them to be able to transfer their SHAREs if they want.</p><p>Maybe we should be tracking totalSharesMinted in the ParentPeer. s_totalShares will need to be updated everytime someone makes a deposit on any chain.</p><p>Ok we’ve got a big issue here. When someone deposits, they need to get minted SHAREs, right? Well the amount that they are minted needs to be proportional to the total value in the system, including accumulated interest. So when someone deposits, not only do we need to message ParentPeer (if Child) and back, but we also need to query the strategy for accumulated interest. The more you build with CCIP, the more apparent the value proposition of Concero becomes.</p><p>Let’s consider this:</p><ul><li>total system usdc is in aave pool on chain b</li><li>user deposits USDC on chain a</li><li>ccip sends message to chain b to query amount (maybe send usdc in same tx)</li><li>ccip sends message back to chain a with amount</li><li>chain a mints user SHAREs</li></ul><p>There will inevitably be time taken between the initial user usdc deposit and actual share mint because of ccip messages, so who will lose out on this? the new user depositing, or the rest of the SHARE holders? If it’s the rest of the SHARE holders, that’s bad. The new user would be preferable to not earn during the message times.</p><p>Maybe the user’s USDC deposit is sent to the strategy, and THEN we message back the shareMintAmount to the deposit chain.</p><p><a href="https://docs.chain.link/ccip/tutorials/evm/usdc">https://docs.chain.link/ccip/tutorials/evm/usdc</a> Good thing there’s a page in the ccip docs for transferring USDC with data to make things easier.</p><h3>CCIP Tx Types 2</h3><p>We will need more than just Deposit, Withdraw and Rebalance tx types. (CCIPTxType will be an enum btw). We will likely need at least DepositInitial and DepositCallback.</p><p>User deposits USDC on chain A → CcipTxType.DepositInitial</p><p>Strategy chain callsback totalValueAmount to mint SHARE → CcipTxType.DepositCallback</p><p>We might even need a third deposit type in case of:</p><ul><li>user deposit on chain A (child)</li><li>needs to update state on chain B (parent)</li><li>needs to deposit on chain C (strategy)</li></ul><p>Maybe we should just be minting SHAREs on parent chain</p><ol><li>User deposits on Chain A (Child)</li><li>USDC tokenAmounts and address(user/depositor) sent to strategy — but HOW do we get the strategy chain from the child????</li></ol><p>Are we seriously going to have to go Child → Parent → Strategy → Parent → Child??? Crosschain is so unbelievably broken. That’s 4 CCIP txs. That’s nearly 2 hours. I mean, once you’re in the system: you’re in. So there is that. This definitely incentivizes people to deposit on the Parent.</p><p><strong>Any child or the parent can be the strategy chain.</strong></p><p>These are the current deposit types I’m entertaining:</p><pre>DepositToParent, // DepositInitialParent<br> DepositToStrategy, // DepositInitialStrategy<br> DepositCallbackParent,<br> DepositCallbackChild,</pre><p>Let’s go through them.</p><h4>DepositToParent</h4><p>This is when a deposit is made on the child chain and the tx needs to pass through the parent to find the strategy. We should have a flag in each child to check if that chain is the strategy.</p><h4>DepositToStrategy</h4><p>This is when a deposit has passed from the child to the parent to get the strategy, or the deposit is made on the parent, and the USDC is being sent to the strategy chain.</p><h4>DepositCallbackParent</h4><p>The USDC has been deposited in the strategy and now we are sending data back to the parent to mint SHAREs to the depositor.</p><h4>DepositCallbackChild</h4><p>USDC has been deposited in the strategy and state has been updated in the parent, and a final message is sent back to the user’s chain to mint their SHAREs.</p><p>Depending on which chain the user has deposited on, they will not have to experience this many crosschain txs.</p><p>But wait, what about if when we rebalance strategies we update where the new strategy is on that Peer? It won’t work because the strategy could hop between a chain of chains. ie chain A still thinks chain B is the current strat, but chain b knows it’s chain c. A flag will have to do.</p><p><strong><em>What happens if someone deposits during a rebalance? Potential known issue. Let’s come back to that.</em></strong></p><h3>Handling Cases</h3><p>We need to handle various cases for CcipTxTypes.</p><p>We have a Parent, Child and Strategy. The parent or any child can be the strategy.</p><p>We need to handle cases where deposits are made on any chain and the strategy is on any chain.</p><p>Let’s think about these cases:</p><p><strong>Deposit made from child</strong></p><ul><li><strong>parent is strategy</strong></li><li>send usdc and depositData from child to parent strategy</li><li>deposit usdc directly into parent strategy</li><li>calculate and send shareMintAmount from parent-strategy to child</li><li><strong>child/deposit is strategy</strong></li><li>deposit usdc directly into this-child-strategy</li><li>ccipMessage to parent, passing amount deposited and totalValue</li><li>ccipReceive from parent, getting shareMintAmount</li><li><strong>chain c is strategy</strong></li><li>send usdc and depositData from child to parent</li><li>send usdc and depositData from parent to strategy (and deposit in strategy)</li><li>send totalValue and depositData from strategy to parent</li><li>send shareMintAmount from parent to child</li></ul><p><strong>Deposit made from parent</strong></p><ul><li><strong>parent is strategy</strong></li><li>deposit usdc directly into parent strategy</li><li>calculate shareMintAmount and mint to depositor</li><li><strong>child is strategy</strong></li><li>send usdc and depositData from parent to strategy-child</li><li>send totalValue and depositData from strategy to parent</li><li>calculate shareMintAmount and mint to depositor</li></ul><h4>We should probably be verifying tokenAmounts for each step.</h4><h3>Deposit into Aave and Compound</h3><p>Aave has this:</p><pre>/**<br>   * @notice Supplies an `amount` of underlying asset into the reserve, receiving in return overlying aTokens.<br>   * - E.g. User supplies 100 USDC and gets in return 100 aUSDC<br>   * @param asset The address of the underlying asset to supply<br>   * @param amount The amount to be supplied<br>   * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user<br>   *   wants to receive them on his own wallet, or a different address if the beneficiary of aTokens<br>   *   is a different wallet<br>   * @param referralCode Code used to register the integrator originating the operation, for potential rewards.<br>   *   0 if the action is executed directly by the user, without any middle-man<br>   */<br>  function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;</pre><p>Compound has this:</p><pre>function supply(address asset, uint256 amount) external;</pre><p>Ok, now how do we get the totalValue in either of these pools? aUSDC.balanceOf(address(this)) for aave? and something similar for compound?</p><p>Comet/Compound v3 has it’s own internal balanceOf() — not a receipt token like aave</p><pre>/**<br>     * @notice Query the current positive base balance of an account or zero<br>     * @dev Note: uses updated interest indices to calculate<br>     * @param account The account whose balance to query<br>     * @return The present day base balance magnitude of the account, if positive<br>     */<br>    function balanceOf(address account) override public view returns (uint256) {<br>        (uint64 baseSupplyIndex_, ) = accruedInterestIndices(getNowInternal() - lastAccrualTime);<br>        int104 principal = userBasic[account].principal;<br>        return principal &gt; 0 ? presentValueSupply(baseSupplyIndex_, unsigned104(principal)) : 0;<br>    }</pre><h3>HelperConfig script</h3><p>Writing the deposit functions has been going well, but they have a lot of paths we need to handle correctly. Before moving on to withdraws and rebalancing, we should write some unit tests for the deposit functionality and dry them a bit.</p><p>We have to go to docs for various contract addresses for our HelperConfig (best place to start if we’re going to be writing tests, so we can just fork mainnets and we don’t have to refactor later).</p><p><a href="https://docs.chain.link/ccip/directory/mainnet/chain/mainnet">https://docs.chain.link/ccip/directory/mainnet/chain/mainnet</a></p><p>Oh right yeah, the Aave docs says to use PoolAddressesProvider to get the pool</p><p><a href="https://aave.com/docs/developers/smart-contracts/pool-addresses-provider">https://aave.com/docs/developers/smart-contracts/pool-addresses-provider</a></p><p>We’ll refactor our contract to use it.</p><p><a href="https://aave.com/docs/resources/addresses">https://aave.com/docs/resources/addresses</a></p><p><a href="https://docs.compound.finance/">https://docs.compound.finance/</a> Compound addresses are here. We can see the cUSDCv3 proxy here: <a href="https://etherscan.io/address/0xc3d688B66703497DAA19211EEdff47f25384cdc3#writeProxyContract">https://etherscan.io/address/0xc3d688B66703497DAA19211EEdff47f25384cdc3#writeProxyContract</a> and the current implementation here: <a href="https://etherscan.io/address/0x40f6c8c12eb5009933449d6b22de094024061821#code">https://etherscan.io/address/0x40f6c8c12eb5009933449d6b22de094024061821#code</a></p><p><a href="https://developers.circle.com/stablecoins/usdc-on-main-networks">https://developers.circle.com/stablecoins/usdc-on-main-networks</a></p><h3>UnsupportedDestinationChain(0) debugging</h3><p>Trying to test basic deposits at the moment. Deposits are successful on the Parent when the Strategy is also on the Parent for both Aave and Compound.</p><p>Now trying to test deposit on Parent when Strategy is on Child. We currently do not have the CLF functionality setting the strategy on the parent, so we’re using a temporary setter which will later be replaced with either a wrapper or the full/simulated CLF calltrace.</p><p>Anyway the internal _setStrategy() which would be called by CLF or the temporary setter.</p><p>Well I fixed it without much to document. That’s happened more than once now, I got stuck, started noting down the process, it was an easy fix, and there was nothing much to document.</p><h3>Foundry panicked and crashed… The path to forking ccipLocalSimulatorFork to work with USDC</h3><p>Good god, look at this error:</p><pre>The application panicked (crashed).<br>Message:  called `Option::unwrap()` on a `None` value<br>Location: /Users/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/revm-19.7.0/src/journaled_state.rs:402</pre><pre>This is a bug. Consider reporting it at &lt;https://github.com/foundry-rs/foundry&gt;</pre><pre>  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━<br>                                ⋮ 8 frames hidden ⋮                               <br>   9: core::panicking::panic::h3ce9043ca357f318<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  10: core::option::unwrap_failed::h84f662fc2717d129<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  11: revm::journaled_state::JournaledState::checkpoint_revert::ha2372507612e4e6e<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  12: core::ops::function::Fn::call::haf5c10b983b13451<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  13: revm::evm::Evm&lt;EXT,DB&gt;::transact::h511e14095f81b969<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  14: foundry_evm::executors::Executor::call_with_env::h154335a27f5af478<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  15: foundry_evm::executors::Executor::call::h782f7de7b909665b<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  16: core::ops::function::impls::&lt;impl core::ops::function::FnMut&lt;A&gt; for &amp;F&gt;::call_mut::h8d82ba17ce23719f<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  17: rayon::iter::plumbing::bridge_producer_consumer::helper::h9d171e5d4c76ff86<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  18: forge::runner::ContractRunner::run_tests::hc7d7ccc88d63e1eb<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  19: forge::multi_runner::MultiContractRunner::run_test_suite::hb4141630c8c82fc8<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  20: rayon::iter::plumbing::bridge_producer_consumer::helper::hfa56bb258a9a151b<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  21: forge::multi_runner::MultiContractRunner::test::h7f9aaca9b2669fcd<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  22: tokio::runtime::task::raw::poll::h43285f4cb434870b<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  23: std::sys::backtrace::__rust_begin_short_backtrace::h6f6727b8eb7b339b<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  24: core::ops::function::FnOnce::call_once{{vtable.shim}}::hc8a24ab009e1ac94<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  25: std::sys::pal::unix::thread::Thread::new::thread_start::h6d53b1b0c047a3b9<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;<br>  26: __pthread_joiner_wake&lt;unknown&gt;<br>      at &lt;unknown source file&gt;:&lt;unknown line&gt;</pre><pre>Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering.<br>Run with RUST_BACKTRACE=full to include source snippets.<br>zsh: abort      forge test --mt test_parent_deposit_strategyIsChild_aave -vvvv</pre><p>Never seen that before. This is happening when trying to switch chains and route a message with the CCIP local simulator fork.</p><p>Someone else had a similar issue in 2023: <a href="https://github.com/foundry-rs/foundry/issues/4572">https://github.com/foundry-rs/foundry/issues/4572</a> and it didnt receive any replies, but said it was fixed. This issue was mentioned in another issue: <a href="https://github.com/foundry-rs/foundry/issues/4586">https://github.com/foundry-rs/foundry/issues/4586</a></p><p>Someone else had a similar error too: <a href="https://github.com/foundry-rs/foundry/issues/4058">https://github.com/foundry-rs/foundry/issues/4058</a> looks like they were doing something with Optimism (same as us) and someone replied saying it’s probably an issue with the RPC URL. If we don’t think of something else we could try replacing the public rpc urls with alchemy ones, but I don’t think that’s our issue because that github issue explicitly mentioned missing trie node.</p><p>So great it looks like we found a bug with Foundry right when we start testing our deposits.</p><p>I tried switching the receiver chain from being opt → eth and it’s still giving the same error.</p><p>How likely is reporting this going to get it fixed before it’s too late?</p><p>I’m annoyed because it’s looking possible we won’t have the exact unit tests I was planning, which would’ve been with fully forked CCIP local simulator facilitating the whole codebase.</p><p>With Chainlink Automation anything can trigger anything. The question is how do you monetize that anything can do anything. What two things need to happen to make money?</p><p>I’ve tried using an older version of foundry. foundryup -i 1.2.0. nope still getting that same error.</p><p>I restarted Cursor and now im getting a different error. Revert] Contract 0x0000000000000000000000000000000000000000 does not exist and is not marked as persistent, see vm.makePersistent()</p><p>We just had to register some stuff with ccipLocalSimulatorFork for the networks being used.</p><p>Now we’re getting a custom error</p><pre>25265] 0x5931822f394baBC2AACF4588E98FC77a9f5aa8C9::releaseOrMint(ReleaseOrMintInV1({ originalSender: 0x000000000000000000000000a8452ec99ce0c64f20701db7dd3abdb607c00496, remoteChainSelector: 4949039107694359620 [4.949e18], receiver: 0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809, amount: 1000000000 [1e9], localToken: 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85, sourcePoolAddress: 0x0000000000000000000000009fcd83bc7f67ada1fb51a4cabea333c72b641bd1, sourcePoolData: 0x0000000000000000000000000000000000000000000000000000000000089e9e0000000000000000000000000000000000000000000000000000000000000003, offchainTokenData: 0x }))<br>    │   │   │   ├─ [7494] 0x55b3FCa23EdDd28b1f5B4a3C7975f63EFd2d06CE::isCursed(0x000000000000000044ae84d8e9a37444) [staticcall]<br>    │   │   │   │   ├─ [2429] 0x4E76D19073eF8c0CE63C2A0034e52745a94db284::isCursed(0x000000000000000044ae84d8e9a37444)<br>    │   │   │   │   │   └─ ← [Return] false<br>    │   │   │   │   └─ ← [Return] false<br>    │   │   │   ├─ [2922] 0x3206695CaE29952f4b0c22a169725a865bc8Ce0f::isOffRamp(4949039107694359620 [4.949e18], 0xEB3d6956BCf7b1E29634C8cd182fC9FA740Bce34) [staticcall]<br>    │   │   │   │   └─ ← [Return] true<br>    │   │   │   └─ ← [Revert] EvmError: Revert<br>    │   │   └─ ← [Revert] custom error 0xe1cd5509</pre><p>This is a side note but I’m concerned about testing with usdc and the local simulator fork, might have to fork the local sim fork’s bnm or the other token.</p><p>Turns out this is directly related to exactly that side note. contract level intuition</p><p>0x5931822f394baBC2AACF4588E98FC77a9f5aa8C9 is the CCIP USDC Token pool contract on Optimism.</p><p>here’s the function that’s reverting</p><p>Cursor is telling me it needs some cctp message attestation which ccip local sim isn’t providing</p><pre>Based on the code and error analysis, I can explain what&#39;s happening and how to fix it:<br>The USDC token pool on Optimism is failing because it&#39;s missing the required CCTP (Cross-Chain Transfer Protocol) message and attestation data. This data is required for USDC cross-chain transfers.<br>The releaseOrMint function expects two key pieces of data in the offchainTokenData parameter:<br>A USDC CCTP message<br>An attestation for that message<br>The message format for USDC CCTP is:<br>Apply to TokenPool.so...<br>116<br>The attestation is a signature that proves the message is valid and came from the source chain.<br>To fix this, you need to:<br>When sending USDC cross-chain, the source chain&#39;s USDC token pool will emit a MessageSent event with the CCTP message.<br>This message needs to be attested by Circle&#39;s attestation service. The service will provide a signature that proves the message is valid.<br>Both the message and attestation need to be passed to the destination chain&#39;s USDC token pool in the offchainTokenData parameter.<br>Looking at your test code in ParentDepositTest.t.sol, you&#39;re using ccipLocalSimulatorFork.switchChainAndRouteMessage(optFork) to simulate the cross-chain message. However, for USDC transfers, you need to:<br>Get the CCTP message from the source chain&#39;s USDC token pool<br>Get an attestation for that message from Circle&#39;s attestation service<br>Pass both to the destination chain&#39;s USDC token pool</pre><p>“I’ll help you with all three steps. Let’s start by modifying the test to properly handle USDC CCTP messages and attestations.</p><p>Now we need to add the switchChainAndRouteMessageWithData function to the CCIPLocalSimulatorFork contract. Let me help you with that:”</p><pre>/**<br>     * @notice Similar to switchChainAndRouteMessage but allows passing custom offchainTokenData<br>     * @param forkId The ID of the destination network fork<br>     * @param offchainTokenData Custom offchain token data to be passed to the offRamp<br>     */<br>    function switchChainAndRouteMessageWithData(uint256 forkId, bytes[] memory offchainTokenData) external {<br>        Internal.EVM2EVMMessage memory message;<br>        Vm.Log[] memory entries = vm.getRecordedLogs();<br>        uint256 length = entries.length;<br>        for (uint256 i; i &lt; length; ++i) {<br>            if (entries[i].topics[0] == CCIPSendRequested.selector) {<br>                message = abi.decode(entries[i].data, (Internal.EVM2EVMMessage));<br>                if (!s_processedMessages[message.messageId]) {<br>                    s_processedMessages[message.messageId] = true;<br>                    break;<br>                }<br>            }<br>        }<br>        vm.selectFork(forkId);<br>        assertEq(vm.activeFork(), forkId);<br>        IRouterFork.OffRamp[] memory offRamps =<br>            IRouterFork(i_register.getNetworkDetails(block.chainid).routerAddress).getOffRamps();<br>        length = offRamps.length;<br>        for (uint256 i = length; i &gt; 0; --i) {<br>            if (offRamps[i - 1].sourceChainSelector == message.sourceChainSelector) {<br>                vm.startPrank(offRamps[i - 1].offRamp);<br>                uint256 numberOfTokens = message.tokenAmounts.length;<br>                uint32[] memory tokenGasOverrides = new uint32[](numberOfTokens);<br>                for (uint256 j; j &lt; numberOfTokens; ++j) {<br>                    tokenGasOverrides[j] = uint32(message.gasLimit);<br>                }<br>                IEVM2EVMOffRampFork(offRamps[i - 1].offRamp).executeSingleMessage(<br>                    message, offchainTokenData, tokenGasOverrides<br>                );<br>                vm.stopPrank();<br>                break;<br>            }<br>        }<br>    }</pre><p>Ok, so it does look like we will be forking ccipLocalSimulatorFork to make it compatible with usdc.</p><p>Alright let’s take a step back from the AI for a second and just look at what we need.</p><p>This is from the USDCTokenPool natspec:</p><pre>/// @dev Only supports version SUPPORTED_USDC_VERSION of the CCTP message format<br>  /// @dev Message format for USDC:<br>  ///     * Field                 Bytes      Type       Index<br>  ///     * version               4          uint32     0<br>  ///     * sourceDomain          4          uint32     4<br>  ///     * destinationDomain     4          uint32     8<br>  ///     * nonce                 8          uint64     12<br>  ///     * sender                32         bytes32    20<br>  ///     * recipient             32         bytes32    52<br>  ///     * destinationCaller     32         bytes32    84<br>  ///     * messageBody           dynamic    bytes      116</pre><p>and that is going to be what is passed first here: _validateMessage(msgAndAttestation.message, sourceTokenDataPayload);</p><p>The message comes from here:</p><pre>MessageAndAttestation memory msgAndAttestation =<br>      abi.decode(releaseOrMintIn.offchainTokenData, (MessageAndAttestation));</pre><p>struct:</p><pre>struct MessageAndAttestation {<br>    bytes message;<br>    bytes attestation;<br>  }</pre><p>uh i think we can get the message directly from the logs</p><p>and then we need the attestation for this:</p><pre>i_messageTransmitterProxy.receiveMessage(msgAndAttestation.message, msgAndAttestation.attestation)</pre><pre>CCTPMessageTransmitterProxy public immutable i_messageTransmitterProxy;</pre><p>takes us here:</p><pre>/// @notice Receives a message from the `IMessageTransmitter` contract and validates it.<br>  /// @dev Can only be called by an allowed caller to process incoming messages.<br>  /// @param message The payload of the message being received.<br>  /// @param attestation The cryptographic proof validating the message.<br>  /// @return success A boolean indicating if the message was successfully processed.<br>  function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success) {<br>    if (!s_allowedCallers.contains(msg.sender)) {<br>      revert Unauthorized(msg.sender);<br>    }<br>    return i_cctpTransmitter.receiveMessage(message, attestation);<br>  }</pre><pre>IMessageTransmitter public immutable i_cctpTransmitter;</pre><p>ok so there’s another implementation somewhere</p><pre>/// @notice Unlocks USDC tokens on the destination chain<br>  /// @param message The original message on the source chain<br>  ///     * Message format:<br>  ///     * Field                 Bytes      Type       Index<br>  ///     * version               4          uint32     0<br>  ///     * sourceDomain          4          uint32     4<br>  ///     * destinationDomain     4          uint32     8<br>  ///     * nonce                 8          uint64     12<br>  ///     * sender                32         bytes32    20<br>  ///     * recipient             32         bytes32    52<br>  ///     * destinationCaller     32         bytes32    84<br>  ///     * messageBody           dynamic    bytes      116<br>  /// param attestation A valid attestation is the concatenated 65-byte signature(s) of<br>  /// exactly `thresholdSignature` signatures, in increasing order of attester address.<br>  /// ***If the attester addresses recovered from signatures are not in increasing order,<br>  /// signature verification will fail.***<br>  /// If incorrect number of signatures or duplicate signatures are supplied,<br>  /// signature verification will fail.<br>  function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool success);</pre><p>i_messageTransmitter on optimism is <a href="https://optimistic.etherscan.io/address/0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8">https://optimistic.etherscan.io/address/0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8</a></p><p>this is on circles github <a href="https://github.com/circlefin/evm-cctp-contracts/blob/6e7513cdb2bee6bb0cddf331fe972600fc5017c9/src/MessageTransmitter.sol#L250">https://github.com/circlefin/evm-cctp-contracts/blob/6e7513cdb2bee6bb0cddf331fe972600fc5017c9/src/MessageTransmitter.sol#L250</a></p><pre>* @param attestation Concatenated 65-byte signature(s) of `message`, in increasing order<br>     * of the attester address recovered from signatures.</pre><p><a href="https://github.com/circlefin/evm-cctp-contracts/blob/6e7513cdb2bee6bb0cddf331fe972600fc5017c9/src/roles/Attestable.sol#L227">https://github.com/circlefin/evm-cctp-contracts/blob/6e7513cdb2bee6bb0cddf331fe972600fc5017c9/src/roles/Attestable.sol#L227</a></p><pre>* @dev Rules for valid attestation:<br>     * 1. length of `_attestation` == 65 (signature length) * signatureThreshold<br>     * 2. addresses recovered from attestation must be in increasing order.<br>     * For example, if signature A is signed by address 0x1..., and signature B<br>     * is signed by address 0x2..., attestation must be passed as AB.<br>     * 3. no duplicate signers<br>     * 4. all signers must be enabled attesters</pre><p>Hmmm ok, so if we can set the enabled attesters ourselves in our tests, then we can just use the foundry cheat for signing.</p><p>here’s the full logic:</p><pre>function _verifyAttestationSignatures(<br>        bytes calldata _message,<br>        bytes calldata _attestation<br>    ) internal view {<br>        require(<br>            _attestation.length == signatureLength * signatureThreshold,<br>            &quot;Invalid attestation length&quot;<br>        );<br>        // (Attesters cannot be address(0))<br>        address _latestAttesterAddress = address(0);<br>        // Address recovered from signatures must be in increasing order, to prevent duplicates<br>        bytes32 _digest = keccak256(_message);<br>        for (uint256 i; i &lt; signatureThreshold; ++i) {<br>            bytes memory _signature = _attestation[i * signatureLength:i *<br>                signatureLength +<br>                signatureLength];<br>            address _recoveredAttester = _recoverAttesterSignature(<br>                _digest,<br>                _signature<br>            );<br>            // Signatures must be in increasing order of address, and may not duplicate signatures from same address<br>            require(<br>                _recoveredAttester &gt; _latestAttesterAddress,<br>                &quot;Invalid signature order or dupe&quot;<br>            );<br>            require(<br>                isEnabledAttester(_recoveredAttester),<br>                &quot;Invalid signature: not attester&quot;<br>            );<br>            _latestAttesterAddress = _recoveredAttester;<br>        }<br>    }</pre><p>Hoho looks like we have this:</p><pre>/**<br>     * @notice Enables an attester<br>     * @dev Only callable by attesterManager. New attester must be nonzero, and currently disabled.<br>     * @param newAttester attester to enable<br>     */<br>    function enableAttester(address newAttester) public onlyAttesterManager {<br>        _enableAttester(newAttester);<br>    }</pre><p>and this:</p><pre>function updateAttesterManager(<br>        address newAttesterManager<br>    ) external onlyOwner {<br>        _setAttesterManager(newAttesterManager);<br>    }</pre><p>so we just _prank(messageTransmitter.owner()) into setting a new attester manager and then enableAttesters</p><p>We are violating this requirement in the signature validation:</p><pre>require(<br>            _attestation.length == signatureLength * signatureThreshold,<br>            &quot;Invalid attestation length&quot;<br>        );</pre><p>it looks like we can set the signatureThreshold here:</p><pre>function setSignatureThreshold(<br>        uint256 newSignatureThreshold<br>    ) external onlyAttesterManager {<br>        _setSignatureThreshold(newSignatureThreshold);<br>    }</pre><p>Alright now we’re getting this error:</p><pre>[9629] 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8::receiveMessage(0x000000000000000300000002000000000008a34c00000000000000000000000019330d10d9cc8751218eaf51e8885d058642e08a0000000000000000000000002b4069517957735be00cee0fadae88a26365528f0000000000000000000000005931822f394babc2aacf4588e98fc77a9f5aa8c900000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000deb1e9a6be7baf84208bb6e10ac9f9bbe1d70809000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000009fcd83bc7f67ada1fb51a4cabea333c72b641bd1, 0x46f25e0bbf6d5c3e7ae163609cb2fa40d7b1a2c317c2ea19470a04fd24d8f7cf1d06211827e9aad4850e7096bdf17a929afb45fab898e27390f6102979267d8e1b4c4608b14f50bff9f0d410649f8944917c318635fce304c2b87e703b719246110260aebdae8d8b25bbef3149799c7f40d0edba5ae44b374ceb6c4b809b5c51a31c41fb1d35d25e80e10f7c6ecde31e1f1bf78ae3896ac80dff665f140a20b2d8a90cd2afb4e526267f5d673cfea48eedbd15dd397a0a67fb5b603f99f69131c76b1c96c6f1d1fb12f120f10d57d5bea03cc3f25339ac3925089efd440fd06af177755bd53eaeb54ed232de030f763e3fb1dacf157431fbd653648ead38441c9f5eef1c)<br>    │   ├─ [3000] PRECOMPILES::ecrecover(0x5e31ccb367219843ab7348ad6b92e66c67e3ec6555be5d1b7167207216e97c08, 27, 32090125471380663783919729957721618702990925299666511819881923089833890740175, 13127902100174864198037556882014640611685641739399205196949603256843815517582) [staticcall]<br>    │   │   └─ ← [Return] 0x00000000000000000000000002f7958355d959f8710ac9fc2338af1506840261<br>    │   └─ ← [Revert] Invalid signature: not attester<br>    ├─ [0] console::log(&quot;Message receive failed with reason:&quot;, &quot;Invalid signature: not attester&quot;)</pre><p>the source of this in the cctp message transmitter signature validation is here:</p><pre>require(<br>                isEnabledAttester(_recoveredAttester),<br>                &quot;Invalid signature: not attester&quot;<br>            );</pre><p>let’s look at the logic for how the _recoveredAttester is recovered:</p><pre>address _recoveredAttester = _recoverAttesterSignature(<br>                _digest,<br>                _signature<br>            );<br>// Signatures must be in increasing order of address, and may not duplicate signatures from same address<br>            require(<br>                _recoveredAttester &gt; _latestAttesterAddress,<br>                &quot;Invalid signature order or dupe&quot;<br>            );<br>            require(<br>                isEnabledAttester(_recoveredAttester),<br>                &quot;Invalid signature: not attester&quot;<br>            );</pre><pre>/**<br>     * @notice Checks that signature was signed by attester<br>     * @param _digest message hash<br>     * @param _signature message signature<br>     * @return address of recovered signer<br>     **/<br>    function _recoverAttesterSignature(<br>        bytes32 _digest,<br>        bytes memory _signature<br>    ) internal pure returns (address) {<br>        return (ECDSA.recover(_digest, _signature));<br>    }<br>    <br>    import &quot;@openzeppelin/contracts/cryptography/ECDSA.sol&quot;;</pre><p>With certora and invariants, you’re thinking of things that are always true, no matter what. Invariants is thinking about what is always true. An invariant is always true. A mathematical expression</p><p>this is from the contract:</p><ol><li>length of _attestation == 65 (signature length) * signatureThreshold</li><li>addresses recovered from attestation must be in increasing order.</li><li>For example, if signature A is signed by address 0x1…, and signature B</li><li>is signed by address 0x2…, attestation must be passed as AB.</li><li>no duplicate signers</li><li>all signers must be enabled attesters</li></ol><pre>Message receive failed with reason: Invalid caller for message</pre><p>Nice we are getting somewhere. How did I do that? I removed the Eth message prefix thing from the attestation signature.</p><pre>// Validate destination caller<br>        if (_msg._destinationCaller() != bytes32(0)) {<br>            require(<br>                _msg._destinationCaller() ==<br>                    Message.addressToBytes32(msg.sender),<br>                &quot;Invalid caller for message&quot;<br>            );<br>        }</pre><p>so this is happening in the cctp message transmitter. I should apply for a grant from chainlink for this, im making their local ccip sim fork tool compatible with usdc</p><p>the USDCTokenPool calls depositForBurnWithCaller, it sets the destinationCaller parameter to domain.allowedCaller. This is the key piece we’re missing.</p><ol><li>The CCTPMessageTransmitterProxy contract has a list of allowed callers that can call receiveMessage</li><li>The USDCTokenPool contract has a Domain struct that includes an allowedCaller field, which is used when calling depositForBurnWithCaller</li></ol><p>The allowedCaller in the domain config must match the address of the CCTPMessageTransmitterProxy on the destination chain, encoded as a bytes32</p><pre>/// @notice Sets the CCTP domain for a CCIP chain selector.<br>  /// @dev Must verify mapping of selectors -&gt; (domain, caller) offchain.<br>  function setDomains(<br>    DomainUpdate[] calldata domains<br>  ) external onlyOwner</pre><p>lovely.</p><ol><li>The allowedCaller must be the MessageTransmitterProxy address of the destination chain</li><li>The domainIdentifier must be the correct USDC domain ID for the destination chain</li><li>The destChainSelector must be the CCIP chain selector of the destination chain</li><li>The configuration must be set on both the source and destination chain’s USDCTokenPools</li></ol><p><a href="https://developers.circle.com/stablecoins/supported-domains">https://developers.circle.com/stablecoins/supported-domains</a></p><p>assuming this is what a domainIdentifier is</p><p>it’s very useful and quite fun having people with the right knowledge and experience challenge your ideas</p><p><a href="https://developers.circle.com/stablecoins/evm-smart-contracts">https://developers.circle.com/stablecoins/evm-smart-contracts</a></p><h3>Share CCT</h3><p>You can think of it as a crosschain wrapper for yield bearing money</p><p>the frontend should just be a single button in the middle. how much do you want to deposit? boom done. money money money money money.</p><h3>Back to the integrating usdc/cctp with ccip local thing</h3><p>ok so looks like we need to do this thing:</p><pre>/// @notice Configures the allowed callers for the `receiveMessage` function.<br>  /// @param configArgs An array of `AllowedCallerConfigArgs` structs.<br>  function configureAllowedCallers(<br>    AllowedCallerConfigArgs[] calldata configArgs<br>  ) external onlyOwner {<br>    for (uint256 i = 0; i &lt; configArgs.length; ++i) {<br>      if (configArgs[i].allowed) {<br>        if (s_allowedCallers.add(configArgs[i].caller)) {<br>          emit AllowedCallerAdded(configArgs[i].caller);<br>        }<br>      } else {<br>        if (s_allowedCallers.remove(configArgs[i].caller)) {<br>          emit AllowedCallerRemoved(configArgs[i].caller);<br>        }<br>      }<br>    }<br>  }</pre><pre>struct AllowedCallerConfigArgs {<br>    address caller;<br>    bool allowed;<br>  }</pre><p>ok we’re configuring but why are we reverting?</p><pre>[248] 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca::configureAllowedCallers([AllowedCallerConfigArgs({ caller: 0x9fCd83bC7F67ADa1fB51a4caBEa333c72B641bd1, allowed: true })])<br>    │   └─ ← [Revert] EvmError: Revert</pre><p>“EvmError: Revert” great, very helpful. let’s go back to CCTPMessageTransmitterProxy::configureAllowedCallers</p><p>wait a minute that’s not the right contract.</p><pre>IMessageTransmitter internal arbCCTPMessageTransmitter =<br>        IMessageTransmitter(0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca);</pre><p>that’s the CCTP deployment, we need the chainlink deployment</p><p>where is the CCTPMessageTransmitterProxy contract address?</p><p>wait a minute</p><p>we are going in the wrong direction. we dont need to be configuring those callers on that contract…i think… what we need is to be doing something with that weird message that gets sent through cctp. the one with the weird formatting listed out in the natspec</p><pre>* Message format:<br>     * Field                 Bytes      Type       Index<br>     * version               4          uint32     0<br>     * sourceDomain          4          uint32     4<br>     * destinationDomain     4          uint32     8<br>     * nonce                 8          uint64     12<br>     * sender                32         bytes32    20<br>     * recipient             32         bytes32    52<br>     * messageBody           dynamic    bytes      84<br>     * @param message Message bytes<br>     * @param attestation Concatenated 65-byte signature(s) of `message`, in increasing order<br>     * of the attester address recovered from signatures.<br>     * @return success bool, true if successful<br>     */<br>    function receiveMessage(<br>        bytes calldata message,<br>        bytes calldata attestation<br>    ) external override whenNotPaused returns (bool success) {<br>        // Validate each signature in the attestation<br>        _verifyAttestationSignatures(message, attestation);  <br>        bytes29 _msg = message.ref(0);<br>        // Validate message format<br>        _msg._validateMessageFormat();<br>        // Validate domain<br>        require(<br>            _msg._destinationDomain() == localDomain,<br>            &quot;Invalid destination domain&quot;<br>        );<br>        // Validate destination caller<br>        if (_msg._destinationCaller() != bytes32(0)) {<br>            require(<br>                _msg._destinationCaller() ==<br>                    Message.addressToBytes32(msg.sender),<br>                &quot;Invalid caller for message&quot;<br>            );<br>        }</pre><p>this is from Message.sol in the cctp codebase:</p><pre>function _destinationCaller(bytes29 _message)<br>        internal<br>        pure<br>        returns (bytes32)<br>    {<br>        return _message.index(DESTINATION_CALLER_INDEX, 32);<br>    }</pre><p>alright we implemented a helper function to parse the dest caller</p><pre>function parseDestinationCaller(bytes memory message) internal pure returns (bytes32) {<br>        // DESTINATION_CALLER_INDEX is 84, and it&#39;s 32 bytes long<br>        bytes32 destinationCaller;<br>        assembly {<br>            destinationCaller := mload(add(message, 116)) // 84 + 32 = 116 to get the start of the field<br>        }<br>        return destinationCaller;<br>    }</pre><p>and our logs reveal this: Destination caller: 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8</p><p>which is the MessageTransmitter we are trying to call receiveMessage on.</p><p>well if we prank to that: _changePrank(address(uint160(uint256(destinationCaller)))); then the message is received successfully and the childPeer gets minted the correct amount of usdc. that’s nice. but we still need to figure out: what is happening under the hood with the local ccip simulator handling the offchainTokenData which includes the cctp msgAndAttestation</p><p>what we want is to initiate the crosschain tx with this: arbParentPeer.deposit(DEPOSIT_AMOUNT);</p><p>and to route it with</p><p>ccipLocalSimulatorFork.switchChainAndRouteMessageWithData(optFork, offchainTokenData);</p><p>but the message isnt going through properly on the other end and the logs dont reveal the cause.</p><p>the offchainTokenData is being used in this bit:</p><pre>IEVM2EVMOffRampFork(offRamps[i - 1].offRamp).executeSingleMessage(<br>                    message, offchainTokenData, tokenGasOverrides<br>                );</pre><p>the address being called is the offRamp, stored here:</p><pre>/**<br>     * @notice Structure representing an offRamp configuration<br>     *<br>     * @param sourceChainSelector - The chain selector for the source chain<br>     * @param offRamp - The address of the offRamp contract<br>     */<br>    struct OffRamp {<br>        uint64 sourceChainSelector;<br>        address offRamp;<br>    }</pre><p>perhaps we need to register the offRamp address with ccip local sim register……..</p><pre>struct NetworkDetails {<br>        uint64 chainSelector;<br>        address routerAddress;<br>        address linkAddress;<br>        address wrappedNativeAddress;<br>        address ccipBnMAddress;<br>        address ccipLnMAddress;<br>        address rmnProxyAddress;<br>        address registryModuleOwnerCustomAddress;<br>        address tokenAdminRegistryAddress;<br>    }</pre><p>nope, doesnt look like the offRamp.</p><p>we’re getting offRamp address from here</p><pre>IRouterFork.OffRamp[] memory offRamps =<br>            IRouterFork(i_register.getNetworkDetails(block.chainid).routerAddress).getOffRamps();<br>        length = offRamps.length;</pre><p>This is the goal: the usdc MUST reach the childPeer on opt via ccip local sim.</p><p>things we know: the attestation is correct.</p><p>the message is correct.</p><p>the usdc is being sent through ccip on the src chain (arb)</p><p>but it isnt reaching the destination chain (opt)</p><p>the logs show us we are switching chains to opt and getting the network details for opt, including the ccip Router.</p><p>i dont see this happening in the logs:</p><pre>for (uint256 i = length; i &gt; 0; --i) {<br>            if (offRamps[i - 1].sourceChainSelector == message.sourceChainSelector) {<br>                vm.startPrank(offRamps[i - 1].offRamp);<br>                uint256 numberOfTokens = message.tokenAmounts.length;<br>                bytes[] memory offchainTokenData = new bytes[](numberOfTokens);<br>                uint32[] memory tokenGasOverrides = new uint32[](numberOfTokens);<br>                for (uint256 j; j &lt; numberOfTokens; ++j) {<br>                    tokenGasOverrides[j] = uint32(message.gasLimit);<br>                }<br>                IEVM2EVMOffRampFork(offRamps[i - 1].offRamp).executeSingleMessage(<br>                    message, offchainTokenData, tokenGasOverrides<br>                );<br>                vm.stopPrank();<br>                break;<br>            }<br>        }</pre><p><strong>Cursor said this</strong>:</p><p>The code is iterating through the offRamps array in reverse order (for (uint256 i = length; i &gt; 0; — i)), looking for a matching sourceChainSelector.</p><p>What should happen:</p><ol><li>The message is sent from Arbitrum (source chain)</li><li>The code switches to Optimism (destination chain)</li><li>It gets the offRamps from Optimism’s router</li><li>It looks for an offRamp that has Arbitrum’s chainSelector as its sourceChainSelector</li><li>The message is sent from Arbitrum (source chain)</li><li>The code switches to Optimism (destination chain)</li><li>It gets the offRamps from Optimism’s router</li><li>It looks for an offRamp that has Arbitrum’s chainSelector as its sourceChainSelector</li></ol><p>The key part is this check:</p><p>if (offRamps[i — 1].sourceChainSelector == message.sourceChainSelector)</p><p>This means we need to find an offRamp in Optimism’s router that is specifically configured to receive messages from Arbitrum. If this match isn’t found, the message won’t be executed.</p><p>well this is the arb selector and inbound offramp on opt for arb: sourceChainSelector: 4949039107694359620 [4.949e18], offRamp: 0xEB3d6956BCf7b1E29634C8cd182fC9FA740Bce34</p><pre>Logs:<br>  Destination caller: 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8<br>  Message hash (hex):<br>  0x4d7da1c326405e1571e175b34218e8a2e2621ff1ca22b78ac6a7e7d2419be0b3<br>  Comparing offRamp sourceChainSelector: 124615329519749607<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 1224752112135636129<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 7613811247471741961<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 1923510103922296319<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 3993510008929295315<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 9027416829622342829<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 1346049177634351622<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 1673871237479749969<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 1562403441176082196<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 15971525489660198786<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 11344663589394136015<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 7264351850409363825<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 6433500567565415381<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 5009297550715157269<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 4949039107694359620<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 4051577828743386545<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 465200170687744372<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 5142893604156789321<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 7264351850409363825<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 465200170687744372<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 11344663589394136015<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 6433500567565415381<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 5142893604156789321<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 4949039107694359620<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 15971525489660198786<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 5009297550715157269<br>  Message sourceChainSelector: 0<br>  Comparing offRamp sourceChainSelector: 4051577828743386545<br>  Message sourceChainSelector: 0</pre><p>well i think we’ve found our problem. why on earth is the src selector 0? Good thing we can see 4949039107694359620 in the list though!</p><p>this is not going well</p><p>the CCIPSendRequested event is emitted in the logs, but it is not being correctly identified in the local sim getRecordedLogs.</p><p>the event in the local sim looks like this:</p><pre>event CCIPSendRequested(Internal.EVM2EVMMessage message);</pre><p>now heres a problem, the latest version of smartcontractkit/chainlink doesnt include that struct in Internal.sol</p><p>it’s included in earlier versions such as that the local sim is based on</p><pre>struct EVM2EVMMessage {<br>    uint64 sourceChainSelector; // ────────╮ the chain selector of the source chain, note: not chainId<br>    address sender; // ────────────────────╯ sender address on the source chain<br>    address receiver; // ──────────────────╮ receiver address on the destination chain<br>    uint64 sequenceNumber; // ─────────────╯ sequence number, not unique across lanes<br>    uint256 gasLimit; //                     user supplied maximum gas amount available for dest chain execution<br>    bool strict; // ───────────────────────╮ DEPRECATED<br>    uint64 nonce; //                       │ nonce for this lane for this sender, not unique across senders/lanes<br>    address feeToken; // ──────────────────╯ fee token<br>    uint256 feeTokenAmount; //               fee token amount<br>    bytes data; //                           arbitrary data payload supplied by the message sender<br>    Client.EVMTokenAmount[] tokenAmounts; // array of tokens and amounts to transfer<br>    bytes[] sourceTokenData; //              array of token data, one per token<br>    bytes32 messageId; //                    a hash of the message data<br>  }<br>  <br>  <br>  Client.EVMTokenAmount<br>  struct EVMTokenAmount {<br>    address token; // token address on the local chain.<br>    uint256 amount; // Amount of tokens.<br>  }</pre><p>these are giving different outputs,</p><pre>console2.log(&quot;Expected:&quot;);<br>            console2.logBytes32(CCIPSendRequested.selector);<br>            console2.logBytes32(keccak256(&quot;CCIPSendRequested((uint64,address,address,uint64,uint256,bool,uint256,address,uint256,bytes,tuple[],bytes[],bytes32))&quot;));</pre><p>will need to double check that second one</p><p>this is the exact event we are looking for:</p><pre>emit CCIPSendRequested(message: EVM2EVMMessage({ sourceChainSelector: 4949039107694359620 [4.949e18], sender: 0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496, receiver: 0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809, sequenceNumber: 828, gasLimit: 0, strict: false, nonce: 0, feeToken: 0xf97f4df75117a78c1A5a0DBb814Af92458539FB4, feeTokenAmount: 16328573801608606 [1.632e16], data: 0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000001ffc33f5e217b1cf95e713db49fcd86c8195666c000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044ae84d8e9a37444, tokenAmounts: [EVMTokenAmount({ token: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, amount: 1000000000 [1e9] })], sourceTokenData: [0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002bf2000000000000000000000000000000000000000000000000000000000000000200000000000000000000000009fcd83bc7f67ada1fb51a4cabea333c72b641bd100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000008b1bd0000000000000000000000000000000000000000000000000000000000000003], messageId: 0x9b15a7ffbda31d57d616caa6ce79324be5b2a65ef98cb0d239bea50b14a0e4e2 }))</pre><pre>console2.logBytes32(keccak256(&quot;CCIPSendRequested((uint64,address,address,uint64,uint256,bool,uint64,address,uint256,bytes,(address,uint256)[],bytes[],bytes32))&quot;));</pre><p>well i guess the .selector was correct</p><pre>0xd0c3c799bf9e2639de44391e7f524d229b2b55f5b1ea94b2bf7da42f7243dddd<br>  0xd0c3c799bf9e2639de44391e7f524d229b2b55f5b1ea94b2bf7da42f7243dddd</pre><p>the original switchChain and route blah blah is finding the event so why isnt our fork?? i think it was because we were breaking when the message sent event was found before the ccipsend requested event</p><p>oh good we’re finally getting somewhere. our fork of the local ccip sim is moving usdc across chains, now we’re back to the invalid caller error on the MessageTransmitter side.</p><p>alright so Destination caller: 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8</p><p>that looks like the opt contract we are trying to call again.</p><p>if we cant prank that address because the ramp is pranked then maybe we need to intercept and redefine the cctp message</p><p>ok we managed that by adding these lines into the loop where the offramp on the dstchain does it’s thing:</p><pre>cctpMessage = _replaceDestinationCaller(cctpMessage, offRamps[i - 1].offRamp);<br>                bytes[] memory offchainTokenData = _createOffchainTokenData(cctpMessage, attesters, attesterPks);<br>                <br>function _replaceDestinationCaller(bytes memory cctpMessage, address destinationCaller) internal pure returns (bytes memory) {<br>        bytes32 destinationCallerBytes32 = bytes32(uint256(uint160(destinationCaller)));<br>        bytes memory newCctpMessage = new bytes(cctpMessage.length);<br>        assembly {<br>            mstore(add(newCctpMessage, 116), destinationCallerBytes32) // 84 + 32 = 116 to get the start of the field<br>        }<br>    }</pre><p>now we’re getting this error: [Revert] InvalidSourceDomain(3, 0)</p><p>wait actually maybe we didnt fix the destCaller thing, maybe this error is happening before getting to that</p><p>ok this is from the usdc token pool error InvalidSourceDomain(uint32 expected, uint32 got); so maybee we did fix the dstCaller</p><p>we need to intercept the message again and replace the domainId with the srcChain</p><p>this isnt going well</p><pre>Logs:<br>  Found MessageSent event<br>  Found CCIPSendRequested event<br>  Message decoded successfully<br>  Decoded message sourceChainSelector: 4949039107694359620<br>  CCTP message initial:<br>  0x000000000000000300000002000000000008b27a00000000000000000000000019330d10d9cc8751218eaf51e8885d058642e08a0000000000000000000000002b4069517957735be00cee0fadae88a26365528f0000000000000000000000004d41f22c5a0e5c74090899e5a8fb597a8842b3e800000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000deb1e9a6be7baf84208bb6e10ac9f9bbe1d70809000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000009fcd83bc7f67ada1fb51a4cabea333c72b641bd1<br>  Destination caller: 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8<br>  Source domain: 0<br>  CCTP message after replacing dstCaller:<br>  0x</pre><p>ok this is bad, we are clearly messing up the dstCaller replacement</p><pre>Logs:<br>  Found MessageSent event<br>  Found CCIPSendRequested event<br>  Message decoded successfully<br>  Decoded message sourceChainSelector: 4949039107694359620<br>  CCTP message initial:<br>  0x000000000000000300000002000000000008b27c00000000000000000000000019330d10d9cc8751218eaf51e8885d058642e08a0000000000000000000000002b4069517957735be00cee0fadae88a26365528f0000000000000000000000004d41f22c5a0e5c74090899e5a8fb597a8842b3e800000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000deb1e9a6be7baf84208bb6e10ac9f9bbe1d70809000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000009fcd83bc7f67ada1fb51a4cabea333c72b641bd1<br>  Destination caller: 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8<br>  Source domain: 0<br>  CCTP message after replacing dstCaller:<br>  0x000000000000000300000002000000000008b27c00000000000000000000000019330d10d9cc8751218eaf51e8885d058642e08a0000000000000000000000002b4069517957735be00cee0fadae88a26365528f000000000000000000000000eb3d6956bcf7b1e29634c8cd182fc9fa740bce3400000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000deb1e9a6be7baf84208bb6e10ac9f9bbe1d70809000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000009fcd83bc7f67ada1fb51a4cabea333c72b641bd1</pre><p>this is looking a bit better</p><p>the implementation for replacing the dstCaller is failing though</p><p>lets review some new ideas:</p><ol><li>go through the logs, find the initial cctp MessageSent event and where it’s being emitted from and how the data is formed</li><li>then we can either manipulate the data at the source or copy what is happening</li><li>the dstCaller is the MessageTransmitter we are calling receiveMessage on. so it MUST be getting set somewhere.</li></ol><p>wait a minute. oh really</p><pre>domains[0] = USDCTokenPool.DomainUpdate({<br>            allowedCaller: bytes32(uint256(uint160(address(optCCTPMessageTransmitter)))),<br>            domainIdentifier: 2,<br>            destChainSelector: optChainSelector,<br>            enabled: true<br>        });<br>        domains[1] = USDCTokenPool.DomainUpdate({<br>            allowedCaller: bytes32(uint256(uint160(address(ethCCTPMessageTransmitter)))),<br>            domainIdentifier: 0,<br>            destChainSelector: ethChainSelector,<br>            enabled: true<br>        });<br>        domains[2] = USDCTokenPool.DomainUpdate({<br>            allowedCaller: bytes32(uint256(uint160(address(arbCCTPMessageTransmitter)))),<br>            domainIdentifier: 3,<br>            destChainSelector: arbChainSelector,<br>            enabled: true<br>        });</pre><p>are you serious right now? i spent the last few hours doing all this assembly stuff and the issue was right here in the base test setup????</p><p>right whatever. what do we need to change that address to?</p><p>(I havent verified this, im making an assumption right now that the allowedCaller in that struct is the dstCaller)</p><p>lets find out by changing it real quick</p><pre>domains[0] = USDCTokenPool.DomainUpdate({<br>            allowedCaller: bytes32(uint256(uint160(address(1)))),<br>            domainIdentifier: 2,<br>            destChainSelector: optChainSelector,<br>            enabled: true<br>        });</pre><pre>Destination caller: 0x0000000000000000000000000000000000000001</pre><p>no way. well at least we got it.</p><p>So the msg.sender will be offRamps[i — 1].offRamp. This means we need to set the destinationCaller to be the offRamp address, but aren’t there different offRamp addresses for different lanes? ie arb → opt has a different offRamp address on opt as eth → opt ?</p><p>so we need to figure out all of our lanes and get the offRamps for each scenario</p><p>ok</p><h3>Optimism offRamps</h3><p>arb → opt 0xEB3d6956BCf7b1E29634C8cd182fC9FA740Bce34</p><p>eth → opt 0x9979c2dfEcA9051Cf7f08274d978984B2dB12C60</p><h3>Arb offRamps</h3><p>opt → arb 0x27a971D482335d0f8d1917451390734f7372A4a3</p><p>eth → arb 0x91e46cc5590A4B9182e47f40006140A7077Dec31</p><h3>Eth offRamps</h3><p>arb → eth 0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9</p><p>opt → eth 0x562a2025E60AA19Aa03Ea41D70ea1FD3286d1D3B</p><p>alright lets just try hardcoding the one we need for this to see if it works</p><p>ok it doesnt work, invalid caller, guess the offRamp thing wasnt true</p><p>the offRamp thing may not have worked but i hardcoded whatever address 0x5931822f394baBC2AACF4588E98FC77a9f5aa8C9 this is - it’s the usdc token pool on the dst chain</p><p>and now the failure is happening in the ccipReceive impl in our childPeer!!!!!!!!</p><p>this is amazing, we have integrated cctp/usdc with ccip local sim. this was alot of effort just for a unit test, but tbf its something i’ve needed many times in the last year when testing ccip and always been annoyed it’s not a feature</p><p>ok the problem here was we werent setting the s_ccipGasLimit, so it was 0. and now it’s working! it’s finally working!</p><h3>Back to the actual project</h3><p>we are successfully moving usdc across, but the deposit is staying in the childPeer when it should be getting sent to the strategy, which in this test is the aavePool.</p><p>so why isnt it getting sent to the strategy?</p><p>because the strategyPool is currently set to 0 on that chain</p><p>test_parent_deposit_strategyIsChild_aave is passing. amazing. there’s a nice satisfaction from putting all the pieces together and seeing it behave as intended.</p><p>look at this (we’re using a setter for the strategy because clf trace not implemented)</p><pre>function test_parent_deposit_strategyIsChild_aave() public {<br>        // @review REPLACE THIS WITH A WRAPPER OR ACTUAL CLF CALLTRACE<br>        _selectFork(optFork);<br>        optChildPeer.setStrategy(optChainSelector, IYieldPeer.Protocol.Aave);<br>        _selectFork(arbFork);<br>        arbParentPeer.setStrategy(optChainSelector, IYieldPeer.Protocol.Aave);<br>        uint256 usdcBalanceBefore = arbUsdc.balanceOf(depositor);<br>        /// @dev act<br>        arbParentPeer.deposit(DEPOSIT_AMOUNT);<br>        /// @dev assert depositor&#39;s USDC balance reduced by the deposit amount on parent chain<br>        uint256 usdcBalanceAfter = arbUsdc.balanceOf(depositor);<br>        assertEq(usdcBalanceAfter, usdcBalanceBefore - DEPOSIT_AMOUNT);<br>        /// @dev switch to child chain and route ccip message with USDC to deposit to strategy<br>        ccipLocalSimulatorFork.switchChainAndRouteMessageWithUSDC(optFork, attesters, attesterPks);<br>        /// @dev assert USDC was deposited to Aave on child chain<br>        address aUsdc = _getATokenAddress(optNetworkConfig.aavePoolAddressesProvider, address(optUsdc));<br>        assertEq(IERC20(aUsdc).balanceOf(address(optChildPeer)), DEPOSIT_AMOUNT);<br>        /// @dev switch back to parent chain and route ccip message with totalValue to calculate shareMintAmount<br>        ccipLocalSimulatorFork.switchChainAndRouteMessage(arbFork);<br>        /// @dev assert correct amount of shares minted<br>        uint256 expectedShareMintAmount = DEPOSIT_AMOUNT * INITIAL_SHARE_PRECISION;<br>        assertEq(arbShare.totalSupply(), expectedShareMintAmount);<br>        assertEq(arbShare.balanceOf(depositor), expectedShareMintAmount);<br>        assertEq(arbParentPeer.getTotalShares(), expectedShareMintAmount);<br>    }</pre><p>for the purpose of a deposit on the parent when the child is the strategy, this is what we want. the user makes a single tx, depositing on the parent, the usdc is sent to pool with highest interest, user receives back SHAREs, representing their share of the pool.</p><p>what do we need to test next? the child deposit? are we there now?</p><p>what were the types of deposits a child could do again? <em>SEE HANDLING CASES</em></p><p>we have successfully tested the case a child is the strategy and deposits are made there for both aave and compound protocols</p><p>ok and now we’ve successfully tested the cases where a deposit is made on a child when the parent and another child are the strategies, for both protocols</p><p>i think we can probably move on to withdrawals</p><h3>Withdraw</h3><p>we’re going to use SHARE.transferAndCall() to withdraw (and also to move across chains)</p><p>what sort of CcipTxTypes will we need for withdrawing?</p><ul><li>withdrawToParent</li><li>withdrawToStrategy</li><li>withdrawCallbackParent</li><li>withdrawCallbackChild</li></ul><p><strong>to calculate </strong><strong>usdcWithdrawAmount (aka </strong><strong>amountToWithdraw), we will need </strong><strong>totalValue, </strong><strong>totalShares, </strong><strong>shareBurnAmount</strong></p><p>let’s consider the various cases again.</p><p><strong>Withdraw made from parent</strong></p><ul><li><strong>parent is strategy</strong></li><li>SHAREs are burnt</li><li>totalShares is updated <em>@review - should this happen before or after withdraw?</em></li><li>amountToWithdraw is calculated</li><li>amountToWithdraw withdrawn from strategy pool and sent to withdrawer</li><li><strong>child is strategy</strong></li><li>SHAREs are burnt</li><li>ccip message to parent with sharesBurnt and to update/get totalShares</li><li>callback to child, sharesBurnt is used to calculate amountToWithdraw and totalShares is updated</li></ul><p><strong>Withdraw made from child</strong></p><ul><li><strong>parent is strategy</strong></li><li>shares are burnt</li><li>shareBurnAmount sent to parent strategy</li><li>usdcToWithdraw amount calculated and USDC ccipSend back to child</li><li><strong>child/withdraw is strategy</strong></li><li>shares are burnt</li><li>shareBurnAmount sent to parent and totalShares updated</li><li>totalShares (preupdate amount) and shareBurnAmount sent back to withdraw-child-strategy</li><li>usdcToWithdraw calculated and transferred to withdrawer</li><li><strong>chain c is strategy</strong></li><li>shares are burnt</li><li>shareBurnAmount sent to parent and totalShares updated</li><li>totalShares (preupdate amount) and shareBurnAmount are forwarder to child strategy</li><li>usdcToWithdraw calculated and ccipSend back to child-withdraw chain</li></ul><p>if we’re going via the parent we should really be getting the totalShares value there before doing totalShares -= shareBurnAmount</p><p>and then we want to send the totalShares along with the shareBurnAmount to the strategy where we will get the totalValue, use that to calculate the amountToWithdraw, and then send the usdcAmountToWithdraw back to the withdrawData.chainSelector</p><p>we are successfully withdrawing when the withdraw chain and strategy chain are the parent</p><p>lets see if we can successfully withdraw when the withdraw is the parent but the strategy is a child</p><p>ok we have successfully tested the withdraws made from parent scenarios. time for lunch.</p><pre>[19570] ParentPeer::ccipReceive(Any2EVMMessage({ messageId: 0x5db94a200cb2d9a6b0e4601198ce4167c4bc3378a286f750aa5ecd9c8eb1dc52, sourceChainSelector: 3734403246176062136 [3.734e18], sender: 0x000000000000000000000000deb1e9a6be7baf84208bb6e10ac9f9bbe1d70809, data: 0x0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000002246e37fded1bf7eefafeecc506e0933cec1a42400000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033d343f77863cab8, destTokenAmounts: [] }))<br>    │   │   │   │   ├─ [4639] 0x141fa059441E0ca23ce184B6A78bafD2A517DdE8::getFee(4949039107694359620 [4.949e18], EVM2AnyMessage({ receiver: 0x0000000000000000000000000000000000000000000000000000000000000000, data: 0x0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000002246e37fded1bf7eefafeecc506e0933cec1a42400000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033d343f77863cab8, tokenAmounts: [], feeToken: 0xf97f4df75117a78c1A5a0DBb814Af92458539FB4, extraArgs: 0x181dcf10000000000000000000000000000000000000000000000000000000000007a1200000000000000000000000000000000000000000000000000000000000000001 })) [staticcall]<br>    │   │   │   │   │   └─ ← [Revert] UnsupportedDestinationChain(4949039107694359620 [4.949e18])</pre><p>had this error before and didnt write it down. i think the issue was arb is trying to send to its own chain selector</p><p>alright we are successfully withdrawing from both protocols when the withdraw is initiated on a child and the parent is the strategy</p><p>time for the last withdrawal scenarios! when a different child to the one we are withdrawing from is the strategy.</p><p>ok done.</p><p>next up will be Rebalancing, then CLF/Automation</p><h3>Rebalancing</h3><p><a href="https://x.com/Route2FI/status/1929610156314067093">https://x.com/Route2FI/status/1929610156314067093</a> this could be useful</p><p>lets remember that for later</p><p>back to rebalancing, what do we want to accomplish? for now just successful ccip rebalance tx.</p><p>eventually it will look like this:</p><ol><li>CLA triggers CLF request (on parent chain)</li><li>CLF DON runs script to find best yield opportunity</li><li>clf.fulfillRequest returns strategy (on parent chain)</li><li>parent chain checks if current/old strategy is same as returned</li><li>if it is, return, probably emit event</li><li>parent chain checks if current/old strategy is on parent</li><li>if it is, then update and reallocate to new strategy</li><li>if new strategy is still parent, just move funds there</li><li>otherwise, ccipSend.RebalanceNewStrategy to newStrategy.chainSelector</li><li>if old strategy is not on parent, ccipSend.RebalanceOldStrategy to oldStrategy.chainSelector</li><li>old strategy then reallocates system funds to new strategy</li><li>if new strategy is same chain as old strategy, move funds there</li><li>otherwise, oldStrategy ccipSend.RebalanceNewStrategy to newStrategy.chainSelector</li><li>we’ll need to handle the CCIPReceive cases for RebalanceOldStrategy and RebalanceNewStrategy txs on both child and parent, maybe we can put all the logic in the YieldPeer</li></ol><p>here’s a very messy implementation</p><pre>/// @notice This function sets the strategy on the parent<br>    /// @notice This function uses ccipSend to send the rebalance message to the old strategy<br>    /// @notice Rebalances funds from the old strategy to the new strategy<br>    /// @notice Handles the case where both the old and new strategy are on this chain<br>    /// @notice Handles the case where the old or new strategies are on different chains with ccipSend<br>    /// @param chainSelector The chain selector of the new strategy<br>    /// @param protocol The protocol of the new strategy<br>    function _setStrategy(uint64 chainSelector, Protocol protocol) internal {<br>        /// @dev get the old strategy<br>        Strategy memory oldStrategy = s_strategy;<br>        /// @dev emit event and return if the new strategy is the same as the old strategy<br>        if (oldStrategy.chainSelector == chainSelector &amp;&amp; oldStrategy.protocol == protocol) {<br>            emit CurrentStrategyOptimal(chainSelector, protocol);<br>            return;<br>        }<br>        Strategy memory newStrategy = Strategy({chainSelector: chainSelector, protocol: protocol});<br>        /// @dev set the new strategy on parent<br>        s_strategy = newStrategy;<br>        // if the new strategy is on this chain AND the old strategy was on this chain, but the protocol is different, we need to reallocate funds and update state<br>        if (<br>            chainSelector == i_thisChainSelector &amp;&amp; oldStrategy.chainSelector == i_thisChainSelector<br>                &amp;&amp; protocol != oldStrategy.protocol<br>        ) {<br>            // get current strategy pool<br>            address oldStrategyPool = _getStrategyPool();<br>            // withdraw totalValue from current strategy<br>            uint256 totalValue = _getTotalValueFromStrategy(oldStrategyPool);<br>            _withdrawFromStrategy(oldStrategyPool, totalValue);<br>            // update state<br>            address newStrategyPool = _getStrategyPool(protocol);<br>            s_strategyPool = newStrategyPool;<br>            // deposit totalValue to new strategy<br>            _depositToStrategy(newStrategyPool, i_usdc.balanceOf(address(this)));<br>        }<br>        // if the old strategy is on this chain, but the new strategy is on a different chain, we need to ccipSend funds to the new Strategy<br>        else if (oldStrategy.chainSelector == i_thisChainSelector &amp;&amp; chainSelector != i_thisChainSelector) {<br>            // get current strategy pool<br>            address oldStrategyPool = _getStrategyPool();<br>            // withdraw totalValue from current strategy<br>            // withdraw totalValue from current strategy<br>            uint256 totalValue = _getTotalValueFromStrategy(oldStrategyPool);<br>            _withdrawFromStrategy(oldStrategyPool, totalValue);<br>            // update state<br>            s_strategyPool = address(0);<br>            // ccipSend funds to new strategy - RebalanceNewStrategy<br>            bytes32 ccipMessageId = _ccipSend(<br>                chainSelector, CcipTxType.RebalanceNewStrategy, abi.encode(newStrategy), i_usdc.balanceOf(address(this))<br>            );<br>        }<br>        // if the old strategy is on a different chain, we need to send a message to it to rebalance funds to the new strategy<br>        else if (oldStrategy.chainSelector != chainSelector) {<br>            // ccipSend funds to new strategy - RebalanceOldStrategy<br>            bytes32 ccipMessageId = _ccipSend(<br>                oldStrategy.chainSelector, CcipTxType.RebalanceOldStrategy, abi.encode(newStrategy), ZERO_BRIDGE_AMOUNT<br>            );<br>        }<br>        /// @dev if the new strategy is on this chain or the old strategy was on this chain, set the strategy pool<br>        if (chainSelector == i_thisChainSelector || oldStrategy.chainSelector == i_thisChainSelector) {<br>            _setStrategyPool(chainSelector, protocol);<br>        }<br>    }<br>    <br>    /// @notice Sets the strategy pool for the YieldPeer<br>    /// @param protocol The protocol of the strategy<br>    /// @param chainSelector The chain selector of the strategy<br>    /// @notice If the Strategy is this chain, the strategy pool is set to the Aave or Compound pool<br>    /// @notice If the Strategy is not this chain, the strategy pool is set to address(0)<br>    function _setStrategyPool(uint64 chainSelector, Protocol protocol) internal {<br>        address strategyPool;<br>        if (chainSelector == i_thisChainSelector) {<br>            if (protocol == Protocol.Aave) strategyPool = address(i_aavePoolAddressesProvider);<br>            else if (protocol == Protocol.Compound) strategyPool = address(i_comet);<br>        }<br>        s_strategyPool = strategyPool;<br>        emit StrategyPoolSet(strategyPool);<br>    }</pre><p>lets clean this baby up a bit all the tests are failing now</p><p>this is a mess. WHY DID I NOT GIT COMMIT BEFORE REFACTORING WITH AI</p><p>THIS</p><p>we’re getting this error [Revert] No CCTP message found which means no usdc is being sent, when it should. for this test test_yield_child_deposit_strategyIsChainC_aave this is an awful setback and entirely my own fault for not git commiting</p><p>let’s take this slow and debug what is happening</p><p>the last event to be emitted before the local sim fail is this:</p><p>emit CCIPMessageSent(messageId: 0x758a7acc4c102a7b806544ca4fed47ed9d0711a7418d3da3d796ea09819530dc, txType: 3, amount: 0)</p><p>txType 3 is DepositCallbackChild, assuming enums start at 0</p><p>that shouldnt be right because we are failing on this final line of this snippet:</p><pre>/// @dev act<br>        optChildPeer.deposit(DEPOSIT_AMOUNT);<br>/// @dev assert depositor&#39;s USDC balance reduced by the deposit amount<br>        uint256 usdcBalanceAfter = optUsdc.balanceOf(depositor);<br>        assertEq(usdcBalanceAfter, usdcBalanceBefore - DEPOSIT_AMOUNT);<br>        /// @dev switch to parent chain and route ccip message with USDC to deposit to strategy<br>        ccipLocalSimulatorFork.switchChainAndRouteMessageWithUSDC(arbFork, attesters, attesterPks);<br>        /// @dev switch to third chain and route ccip message with USDC to deposit to strategy<br>        ccipLocalSimulatorFork.switchChainAndRouteMessageWithUSDC(ethFork, attesters, attesterPks);</pre><p>so we successfully send usdc to the parent from the child, but the parent is not properly forwarding it to the strategy</p><pre>CCIPLocalSimulatorFork::switchChainAndRouteMessageWithUSDC(2, [0x235739A3558844a3A6bC892E58160c8866B2F48a, 0x1F8830562d09e0091c45C4497d578698D4CcA495, 0x46b3351cEC2e2894fB86f516571c71af735129cc, 0x98CCf16957d570D7b08ee14cb7f28B7134629A89], [20446281702270753840690766545487606775683492866242999899033812487149029933055 [2.044e76], 49122242058703694249118830405974474269253475590835006894245756864029220552986 [4.912e76], 18324067312630338727698582437758273338129539904811332467964650786258440414685 [1.832e76], 2410372839825060559863049644307281798630208676479422982278344845517011015526 [2.41e75]])</pre><p>2 is the network fork. can we verify this is eth?</p><p>yes</p><p>looks like we’re hitting the wrong condition here:</p><pre>if (strategy.chainSelector == i_thisChainSelector || strategy.chainSelector == depositData.chainSelector) {<br>            depositData.shareMintAmount = _calculateMintAmount(depositData.totalValue, depositData.amount);<br>            s_totalShares += depositData.shareMintAmount;<br>            bytes32 ccipMessageId = _ccipSend(<br>                depositData.chainSelector, CcipTxType.DepositCallbackChild, abi.encode(depositData), ZERO_BRIDGE_AMOUNT<br>            );<br>            // do something with ccipMessageId?<br>        }<br>        /// @dev If Strategy is on third chain, forward deposit to strategy<br>        if (strategy.chainSelector != i_thisChainSelector &amp;&amp; strategy.chainSelector != depositData.chainSelector) {<br>            bytes32 ccipMessageId =<br>                _ccipSend(strategy.chainSelector, CcipTxType.DepositToStrategy, encodedDepositData, depositData.amount);<br>            // do something with ccipMessageId?<br>        }</pre><p>i think i found the problem, we weren’t updating s_strategy.chainSelector in the temporary parent setter. yep. thank goodness for that. that was a bad half hour. better git commit.</p><p>we are back to testing rebalancing.</p><p>we correctly emit event and return if current strategy is optimal</p><p>we correctly rebalance from one parent pool to another</p><p>now we are stuck trying to rebalance from one pool to another on same child chain</p><p>for some reason it looks like we are not withdrawing the totalValue from the first pool (aave), which we want so we can rebalance it to another pool (compound)</p><p>ok all rebalance txs are passing unit tests</p><p>lets move onto chainlink functions</p><h3>Chainlink Functions</h3><p>We are creating a new module contract that will inherit the ParentPeer, just to keep the code cleaner and easier to maintain</p><p>ParentCLF is ParentPeer.</p><p>defi llama has an api that looks like it returns APY data for pools across chains and protocols, this may be alot more simple to use than reading state off every chain</p><p>it seems that the json response from the defi llama api is too large for clf to handle.</p><p>we made a proxy api and deployed it on aws, so i guess we’re going for an aws sponsor prize now</p><p>curl -X POST &lt;[REDACTED]&gt; -H &quot;Content-Type: application/json&quot; -d &#39;{&quot;symbol&quot;:&quot;USDC&quot;,&quot;projects&quot;:[&quot;aave-v3&quot;,&quot;compound-v3&quot;],&quot;chains&quot;:[&quot;Arbitrum&quot;,&quot;Ethereum&quot;,&quot;Base&quot;,&quot;Optimism&quot;]}&#39;</p><p>This is going very well. It’s amazing how quickly the initial prototype for an automated crosschain yield optimizer can be built.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MRjY_FQyKCQ8LhqPFbv-Qw.png" /></figure><p>first coverage report is looking good</p><p>added a few functions related things to the network config and getting stack too deep errors. will probably have to refactor the network config to be comprised of multiple structs</p><p>Chainlink Functions requires the user to sign terms of service to create subscriptions. We could split up a signature and sign it, but seeing as we’re just going to do it through the frontend for the testnet deployments, I don’t see a problem with just pranking the functionsRouter.owner() and bypassing the signature requirement for our fork tests.</p><p>This is annoying:</p><pre>[1008162101] ParentCLF::sendCLFRequest()<br>    │   ├─ [1008146008] 0x97083E831F8F0638855e2A515c90EdCF158DF238::sendRequest(49, 0x6c636f64654c6f636174696f6ec258200000000000000000000000000000000000000000000000000000000000000000686c616e6775616765c25820000000000000000000000000000000000000000000000000000000000000000066736f7572636563313233, 1, 300000 [3e5], 0x66756e2d617262697472756d2d6d61696e6e65742d3100000000000000000000)<br>    │   │   ├─ [1008114796] 0xd93d77789129c584a02B9Fd3BfBA560B2511Ff8A::startRequest((0x6c636f64654c6f636174696f6ec258200000000000000000000000000000000000000000000000000000000000000000686c616e6775616765c25820000000000000000000000000000000000000000000000000000000000000000066736f7572636563313233, 0x0000000000000000000000000000000000000000000000000000000000000000, 0xBb2180ebd78ce97360503434eD37fcf4a1Df61c3, 1000000000000000000000 [1e21], 0, 49, 0, 300000 [3e5], 1, 0, 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38))<br>    │   │   │   ├─ [15643] 0x86E53CF1B870786351Da77A57575e79CB55812CB::latestRoundData() [staticcall]<br>    │   │   │   │   ├─ [7410] 0x9b8DdcF800a7BfCdEbaD6D65514dE59160a2C9CC::latestRoundData() [staticcall]<br>    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000044dab0000000000000000000000000000000000000000000000000000000055d6df940000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000000000000000044dab<br>    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000010000000000044dab0000000000000000000000000000000000000000000000000000000055d6df940000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000010000000000044dab<br>    │   │   │   ├─ [1118] 0x86E53CF1B870786351Da77A57575e79CB55812CB::decimals() [staticcall]<br>    │   │   │   │   ├─ [276] 0x9b8DdcF800a7BfCdEbaD6D65514dE59160a2C9CC::decimals() [staticcall]<br>    │   │   │   │   │   └─ ← [Return] 8<br>    │   │   │   │   └─ ← [Return] 8<br>    │   │   │   ├─ [3143] 0x86E53CF1B870786351Da77A57575e79CB55812CB::latestRoundData() [staticcall]<br>    │   │   │   │   ├─ [1410] 0x9b8DdcF800a7BfCdEbaD6D65514dE59160a2C9CC::latestRoundData() [staticcall]<br>    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000044dab0000000000000000000000000000000000000000000000000000000055d6df940000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000000000000000044dab<br>    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000010000000000044dab0000000000000000000000000000000000000000000000000000000055d6df940000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000000000000068400b310000000000000000000000000000000000000000000000010000000000044dab<br>    │   │   │   ├─ [1118] 0x86E53CF1B870786351Da77A57575e79CB55812CB::decimals() [staticcall]<br>    │   │   │   │   ├─ [276] 0x9b8DdcF800a7BfCdEbaD6D65514dE59160a2C9CC::decimals() [staticcall]<br>    │   │   │   │   │   └─ ← [Return] 8<br>    │   │   │   │   └─ ← [Return] 8<br>    │   │   │   ├─ [0] 0x000000000000000000000000000000000000006C::getPricesInWei() [staticcall]<br>    │   │   │   │   └─ ← [InvalidFEOpcode] EvmError: InvalidFEOpcode</pre><p>what? because we’re forking arbitrum some opcode isnt working in foundry? is that it?</p><p>inconvenient. we could replace arb as our parent chain with base maybe</p><p>yep the problem was arbitrum. going to have to refactor all our tests to use base as the parent now. Wow, refactoring is going very quick with cursor!</p><p>ok chainlink functions send request and fulfill request are working.</p><h3>To Do list:</h3><p>let’s list the things we need to complete this project:</p><ol><li>string constant SOURCE_CODE pointing to code on github</li><li>src.js on github</li><li>should proxy api url be a secret?</li><li>minimise src.js code</li><li>clean up peer contracts</li><li>clean up unit tests</li><li>onTokenTransfer SharePool ERC677Receiver</li><li>unit tests</li><li>fees</li><li>implement other strategy protocols such as euler</li><li>invariant tests</li><li>certora</li><li>deploy on testnets</li><li>register with time-based automation</li><li>frontend</li><li>comprehensive natspec</li><li>demo video</li><li>detailed README</li><li>submit before 29th</li></ol><h3>1. string constant</h3><p>let’s see what concero do <a href="https://github.com/concero/contracts-v1/blob/fa45c941685cbf9816b89a19d2690d4a3365b52c/contracts/InfraCLF.sol#L47">https://github.com/concero/contracts-v1/blob/fa45c941685cbf9816b89a19d2690d4a3365b52c/contracts/InfraCLF.sol#L47</a></p><pre>string private constant CL_JS_CODE =<br>        &quot;try{const m=&#39;&lt;https://raw.githubusercontent.com/&gt;&#39;;const u=m+&#39;ethers-io/ethers.js/v6.10.0/dist/ethers.umd.min.js&#39;;const [t,p]=await Promise.all([ fetch(u),fetch(m+&#39;concero/contracts-v1/&#39;+&#39;release&#39;+`/tasks/CLFScripts/dist/infra/${BigInt(bytesArgs[2])===1n ? &#39;DST&#39;:&#39;SRC&#39;}.min.js`,),]);const [e,c]=await Promise.all([t.text(),p.text()]);const g=async s=&gt;{return(&#39;0x&#39;+Array.from(new Uint8Array(await crypto.subtle.digest(&#39;SHA-256&#39;,new TextEncoder().encode(s)))).map(v=&gt;(&#39;0&#39;+v.toString(16)).slice(-2).toLowerCase()).join(&#39;&#39;));};const r=await g(c);const x=await g(e);const b=bytesArgs[0].toLowerCase();const o=bytesArgs[1].toLowerCase();if(r===b &amp;&amp; x===o){const ethers=new Function(e+&#39;;return ethers;&#39;)();return await eval(c);}throw new Error(`${r}!=${b}||${x}!=${o}`);}catch(e){throw new Error(e.message.slice(0,255));}&quot;;</pre><p>looks like they’re doing some fancy stuff handling different scripts depending on src/dst chain txs. the hash verification thing is nice, but im not sure we strictly need it.</p><p>so we can point to the raw js on github for the clf don to run</p><p>we should make the proxy API secret because I dont want other people spamming it.</p><p>put the proxy api url in a secret gist on github and encrypt it with chainlink functions toolkit</p><p>the script we have on the raw github works in playground, but not if we paste the raw url there.</p><p>trying deploying on base sepolia</p><p>none of the deployments are working. this is getting stressful. javascript is the worst part about using chainlink and verifying contracts on explorers is the worst part of development.</p><p>i suppose i could do something else and come back to this…</p><h3>SharePool ERC677Receiver</h3><p>we are jumping ahead to something easy</p><p>do tokenPools need to hold LINK for ccip fees?</p><h3>Side note</h3><p>invariant: ParentPeer::s_totalShares == ghost_totalSharesMintedAcrossChains</p><p>we can probably track ghost_totalSharesMintedAcrossChains via ccipReceive data, or even events</p><h3>ERC677 CCT,</h3><p>should be redeemable across chains, ie users can transferAndCall it, with a dstChainSelector of their choice to receive their USDC on. if data is left blank, it defaults to current chain/</p><h3>CLF debugging</h3><p>Today we are trying to figure out why CLF won’t work with encrypted secrets and raw gist github link. This is likely to be the worst part of this project. I hate verifying contracts on explorers. I’ve made a basic contract with the absolute bare bones needed to use clf and check the params we are using and the tx isnt going through with cast send, so to debug with tenderly i want to deploy on base sepolia but it wont verify, even when flattening. there has never been a project where i had a worse experience during development than verifying on block explorers. horrible process.</p><p>ok we did it with a flattened file, optimizer runs and removing the “0x” at the start of the abi encoded constructor args. lets get this on tenderly and figure out why clf isnt working</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_CudkifGJqpvkHaR9q8tgw.png" /></figure><p>even though we are passing the constructor args of the clfRouter and clfSubId through our helper config, AND included them in contract verification, the getters are returning 0 for both.</p><p>didnt include this line in helperconfig</p><p>else if (block.chainid == 84532) activeNetworkConfig = getBaseSepoliaConfig();</p><p>………ok</p><p>we are successfully making the request to chainlink functions now. thank goodness. and it is returning an error.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R73J4jCJvQfQVNGA2htOxA.png" /></figure><p>ok this means the problem is the secrets encryption. i put the encrypted secret in a storage slot and added a setter, so can just retry the encryption and change it without having to redeploy (hopefully)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GLnlxoIqgJB0MEvqeJkwjA.png" /></figure><p>interesting ok so there’s a problem with the gist hosted secrets.json?</p><p><strong><em>NOTE: CHANGE FUNCTIONS REQUEST LIBRARY FROM X TO 1</em></strong></p><p>in the encryption script we used a different private key to the one we are using to interact with our contract/CLF… maybe that’s the issue? nope still failed to fetch valid secrets</p><p>I encrypted the {api: “proxyapi.url”} uploaded it to gist like so <a href="https://gist.githubusercontent.com/contract-level/7ee5ca74aa43f85e1ca0c525616e4bd9/raw/6033ef099eaa4089df591187a2f4d4d3fd629de6/encrypted-secrets.json">https://gist.githubusercontent.com/contract-level/7ee5ca74aa43f85e1ca0c525616e4bd9/raw/6033ef099eaa4089df591187a2f4d4d3fd629de6/encrypted-secrets.json</a></p><pre>{<br>    &quot;encryptedSecrets&quot;: &quot;0x7b225444483243747874223a2265794a48636d393163434936496c41794e5459694c434a44496a6f695545465a54314a564d324653564570314b32684256566b77596d4d775156684f636b5651576a686b5530316a4b325a7559556455565578564d4430694c434a4d59574a6c62434936496b464251554642515546425155464251554642515546425155464251554642515546425155464251554642515546425155464251554642515545394969776956534936496b4a5064544252556e557257556868625774785930673461335577656e6873546d7772553268526230383256334a524e464d72646d645561444a794e456772626a525164324d345444685a56466f786232706c57456b765a584273556e6850543035336245685662334e33516d564f556e566f565430694c434a5658324a6863694936496b4a50614559796357686a5646707a64556c584d316732563356705a586456546b6c6d626e686f4b3256584d455a75513046334e7a526b6248424a6558704256585a544e4446525a5563724b7a564b634670514d437475636a41324e3278514e484a42616c517863564a6e5231567a4e6a5631637a30694c434a46496a6f695a335a6f5757707a546c5a70617a426b656a46554d58564756545a6e526d6c59645849305431704d54314a434e335249616d684b64336c6e535430694c434a47496a6f695a6e6b3265544e6d6257743363334231563264304e5441765345743259565a45546e67324b7a5236526c464c6330457854546c485655526d597a306966513d3d222c2253796d43747874223a22775663326c66335a5358446f364a3573716945594749514b54696548646f6d5059737576794e4c516d2f76444a392b636e73615871684a50394c55536f7a4d2f5879586a54474d35393070516e733459626733716c5445425539564d4175433230762b39707a4e4936596c4f383867306952463767516a4e4d50366e70704c7a562f734e444575506d337038336b2b6d5851746a7761566978595949546e75386c5a3846485165774d534d566855424c32666f593734553749713852504254324859353964712f4772334f766e4d35794b7769493462386d63576c6c54516a636f53456f4b474a594e6b313768325232554b4b6358444863645651754768474c3749726d4c785a48685934616666506c6e4d5755447751395479414662667274704248303250353958634931656f6e58656b6d4f5975536255416a47757a50722f6149464f73462b3556495a472b48636975394f636e5239743170547637725a5033376a49446c50442f344b615078754e4c496346364a355846566368374b4272577446616f6a4434594b305a4f5457646877306e6361684a2f68323855587477756e5138386c6d71384552766e7a6e2b4f684b796761686465355552706670327549467273565258623561684847432f2b51624e706975733471466b342b65506f3266584645592f667069314d696b597a4156504a387074576f432b59525856687466564c76783164323634456533576b516455634873437638385a54774e767630422f4b3453786b42774a47364250763634452f4d4b7865514779533665495341784f5637737a53563145785a374664766d53464d5a79342b6f784c2b4c6a6f314c3779427935567a4e553071593644792f744b62544f456a5847413d3d222c224e6f6e6365223a224c794465706b68375766377854475946227d&quot;<br>}</pre><p>then encrypted the gist url,</p><p>0x73184483ace400c49c8591c3fbfc8e9202de8d53b4ac7336364a4544d15f6dd674d4508e7ac8063f6433d795a5a541d3cad4262c778fb9ef7c5f7eda944437465e6707fef1ad0698aee7f78e67d1d746cc0c7a59cf4d8fac4c19ae83c5a4816f573ff65409af783eaac8d68d9d58b5ec84552b0f9034b6288d681fd9967e37601e425d00b9bfe96ae4c2e325b650a607e05f83f0813a82d68566077fa60d4ebbebe6e617acd7b7d111953a67e630c95988a31c3dbed01a8ad43d98b0e5408592fdd4fbe64aa43997fc350a7351b4f79fb0bc6e3cbb0b3843429dab7f0cc96b54d4aab3c6f7117fe5d61940c94effadee46</p><p>and passed that to the contract, and got this error</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XvM8rL7XGIqi2FNloarzJQ.png" /></figure><p>after a few hours of debugging and searching, i got nowhere.</p><p>i deployed a new contract with inline code location and got this error</p><pre>Error:<br>Failed to decrypt secrets URLs</pre><p>so close to giving up on the remote hosted encrypted secrets thing. it’s not essential for our project’s functionality, it would just be nice to have.</p><p>i set the encryptedSecret url again on the newly deployed contract and got this</p><pre>Error:<br>Invalid secrets ownership</pre><p><a href="https://stackoverflow.com/questions/78915151/getting-invalid-secrets-ownership-error-while-using-chainlink-functions-on-eth">https://stackoverflow.com/questions/78915151/getting-invalid-secrets-ownership-error-while-using-chainlink-functions-on-eth</a></p><p>zubin said this “cannot repro error based on this. Also, this may be because the signer of the secrets is different from the person trying to use them? Check the PRIVATE_KEY env var’s value for each of those scripts?”</p><p>so how is this supposed to work if we’re using chainlink automation to send requests? is the “person trying to use them” the address that registered with chainlink functions?</p><p><a href="https://gist.githubusercontent.com/contract-level/7ee5ca74aa43f85e1ca0c525616e4bd9/raw/1a76b23239fe571d507d49edceb1c7274317b2e0/encrypted-secrets.json">https://gist.githubusercontent.com/contract-level/7ee5ca74aa43f85e1ca0c525616e4bd9/raw/1a76b23239fe571d507d49edceb1c7274317b2e0/encrypted-secrets.json</a></p><p>0x1b4e2d1a565a496c987dc6d8303c52370378b6dcecfd8c1a11b7141822be2b3cb96daba5632633858fe0f07fd5ac85b5293101588003a9f260b0c3d1134765de7b9b16bc1d9087affbf117c4b40a259bc618892ade4707190950fd320e239e36e1f16d577e8c9ed52bad67544530dbca3d846dc4b1f3f8eb6c7a6346172762f449be0d5bcfac35dde9a342db2e009d19c87facf05164b6a3ea257c5a687190b38ec8fd117bfc6a27d7da08abb980afb3aca40b71adbf1cf823b6a3b7bda583e3dc76f1e828bb991398c0b0ee96a2b6c5414c86f62e317286480850e80e8d6ab32718640ef44135f7a875b29e757566a823</p><p>ok THAT worked.</p><p>so it works with inline. let’s see what happens if we use that encrypted secret in the remote one</p><p>nope. still getting invalid code location. but at least we now know our encrypted remote hosted secret is working.</p><p>this is the working inline contract btw: <a href="https://sepolia.basescan.org/address/0x48db59de3cd7bc26affe739f6666c7ae3852dbb7#code">https://sepolia.basescan.org/address/0x48db59de3cd7bc26affe739f6666c7ae3852dbb7#code</a></p><p>works with the above hex as encryptedSecret</p><p><a href="https://functions.chain.link/base-sepolia/333#/side-drawer/request/0x2c83c674b5eb724e8bff91ecd37f1e4ee0b4c786cc29f46c9b5c1b4a17b9bcf4">https://functions.chain.link/base-sepolia/333#/side-drawer/request/0x2c83c674b5eb724e8bff91ecd37f1e4ee0b4c786cc29f46c9b5c1b4a17b9bcf4</a></p><p>this is the not working remote location contract: <a href="https://sepolia.basescan.org/address/0x2Aa15cA0d8997004765a33c7c09Ccc395a685754#writeContract">https://sepolia.basescan.org/address/0x2Aa15cA0d8997004765a33c7c09Ccc395a685754#writeContract</a></p><p>maybe lets see if automation will work for the inline contract we just deployed</p><p>ok registered the time based automation. let’s see if it does what it’s supposed to in 9 minutes time. (fingers crossed it sends a clf request to our proxy and then returns the optimal yield strategy)</p><p>we are still compromising on the code location, but made good progress on the remote hosted encrypted secret</p><p>nice automation successfully triggers the clf call and as long as we have the source code inline, then the remote encrypted secrets work and the callback is successful.</p><p>right how are we going to solve this remote invalid location error</p><p>wait a minute</p><p>if we have the url to our source code inline….. does that make it inline rather than remote??</p><p>should we be using inline and the inline code is our remotely hosted code? let’s find out…</p><p>now we get this error</p><p>Exec Error: syntax error, RAM exceeded, or other error</p><p>lets try a minimised script without long variable names or console logs and put it on gist then use that url as the inline source</p><p>nah got same error</p><p>i think maybe we need to wrap the url in some async func in the inline</p><pre>string private constant CLF_JS_CODE = &quot;try { const url = &#39;&lt;https://raw.githubusercontent.com/contractlevel/yield/main/functions/src.js&gt;&#39;; const response = await fetch(url); if (!response.ok) throw new Error(&#39;Fetch failed: &#39; + response.status); const script = await response.text(); const execute = new Function(script + &#39;; return (async () =&gt; { try { return await main(secrets); } catch (e) { return Functions.encodeString(e.message.slice(0, 255)); } })();&#39;); const result = await execute(); return result; } catch (e) { throw new Error(e.message.slice(0, 255)); }&quot;;</pre><pre>string private constant CLF_JS_CODE = &quot;(async () =&gt; { try { const url = &#39;&lt;https://raw.githubusercontent.com/contractlevel/yield/main/functions/src.js&gt;&#39;; const response = await fetch(url); if (!response.ok) throw new Error(&#39;Fetch failed: &#39; + response.status); const script = await response.text(); return await eval(&#39;(async () =&gt; {&#39; + script + &#39;})()&#39;); } catch (e) { return Functions.encodeString(e.message.slice(0, 255)); } })();&quot;;</pre><pre>string private constant CLF_JS_CODE = &quot;(async () =&gt; { try { const url = &#39;&lt;https://raw.githubusercontent.com/contractlevel/yield/main/functions/src.js&gt;&#39;; const response = await fetch(url); if (!response.ok) throw new Error(&#39;Fetch failed: &#39; + response.status); const script = await response.text(); const fn = new Function(&#39;secrets&#39;, &#39;Functions&#39;, script + &#39;; return main(secrets);&#39;); return await fn(secrets, Functions); } catch (e) { return Functions.encodeString(e.message.slice(0, 255)); } })();&quot;;</pre><p>OK WE are successful if we use this inline: try { const r = await fetch(&#39;&lt;https://gist.githubusercontent.com/contract-level/9abd2ba2d8afdf02f4ef6a8b0761d5f0/raw/558d3959d61da95382abddf28a69a9fc63bdab42/async-src-min.js&gt;&#39;); if (!r.ok) throw Error(&#39;F:&#39; + r.status); return eval(await r.text()); } catch (e) { return Functions.encodeString(e.message.slice(0,99)); }</p><p>the remote url points here (async () =&gt; {if(!secrets.api)throw Error(&#39;API URL missing&#39;);const c={Arbitrum:&#39;4949039107694359620&#39;,Ethereum:&#39;5009297550715157269&#39;,Base:&#39;15971525489660198786&#39;,Optimism:&#39;3734403246176062136&#39;};try{const r=await Functions.makeHttpRequest({url:secrets.api,method:&#39;POST&#39;,headers:{&#39;Content-Type&#39;:&#39;application/json&#39;},data:{symbol:&#39;USDC&#39;,projects:[&#39;aave-v3&#39;,&#39;compound-v3&#39;],chains:Object.keys(c)},timeout:9000});if(r.error||!r.data||!r.data.chain)return Functions.encodeString(JSON.stringify(r));const p=r.data;if(!Object.keys(c).includes(p.chain)||![&#39;aave-v3&#39;,&#39;compound-v3&#39;].includes(p.project)){const s=Functions.encodeUint256(BigInt(0)),e=Functions.encodeUint256(0),t=new Uint8Array(64);return t.set(s,0),t.set(e,32),t}const e=p.project===&#39;aave-v3&#39;?0:1,s=Functions.encodeUint256(BigInt(c[p.chain]||&#39;0&#39;)),t=new Uint8Array(64);return t.set(s,0),t.set(e,32),t}catch(e){return Functions.encodeString(e.message)}})();</p><p>this was a lot of work to optimise our inline constant</p><p>let’s go back to our todo list</p><p>hang on this isnt working properly on our actual contract now</p><p>so the encrypted secret bytes isnt working as a constant, but it works if it’s read from storage….that’s inconvenient but its not the end of the world</p><p>if we cache encryptedSecret = ENCRYPTED_SECRET in the function call, that doesnt work either.</p><p>we could use custom logic automation to read from storage and pass it to the clf request when block.timestamp ≥ s_lastRebalance + 1 days.</p><p>ok all we had to do was make the constant look like this:</p><pre>bytes internal constant ENCRYPTED_SECRET =<br>        hex&quot;1b4e2d1a565a496c987dc6d8303c52370378b6dcecfd8c1a11b7141822be2b3cb96daba5632633858fe0f07fd5ac85b5293101588003a9f260b0c3d1134765de7b9b16bc1d9087affbf117c4b40a259bc618892ade4707190950fd320e239e36e1f16d577e8c9ed52bad67544530dbca3d846dc4b1f3f8eb6c7a6346172762f449be0d5bcfac35dde9a342db2e009d19c87facf05164b6a3ea257c5a687190b38ec8fd117bfc6a27d7da08abb980afb3aca40b71adbf1cf823b6a3b7bda583e3dc76f1e828bb991398c0b0ee96a2b6c5414c86f62e317286480850e80e8d6ab32718640ef44135f7a875b29e757566a823&quot;;</pre><p>NOW lets go back to our to do list.</p><h3>To Do list 2</h3><ol><li>string constant SOURCE_CODE pointing to code on github</li><li>src.js on github</li><li>should proxy api url be a secret? yes and it is</li><li>minimise src.js code</li><li>clean up peer contracts</li><li>clean up unit tests</li><li>onTokenTransfer SharePool ERC677Receiver</li><li>unit tests</li><li>rename Share to YieldCoin? YieldPool?</li><li>fees</li><li>implement other strategy protocols such as euler</li><li>invariant tests</li><li>certora</li><li>deploy on testnets</li><li>register with time-based automation</li><li>frontend</li><li>optional: SOLANA CCIP?? depends how much time we have</li><li>comprehensive natspec</li><li>demo video</li><li>detailed README</li><li>submit before 29th</li></ol><h3>YieldCoin crosschain tests</h3><p>might change SHARE to YIELD or YieldCoin (<em>”it only goes up!”</em>)</p><p>TokenPool.sol:</p><pre>/// @notice Checks if the pool address is configured on the remote chain.<br>  /// @param remoteChainSelector Remote chain selector.<br>  /// @param remotePoolAddress The address of the remote pool.<br>  function isRemotePool(uint64 remoteChainSelector, bytes calldata remotePoolAddress) public view returns (bool) {<br>    return s_remoteChainConfigs[remoteChainSelector].remotePools.contains(keccak256(remotePoolAddress));<br>  }<br>  <br>    /// @dev A set of allowed chain selectors. We want the allowlist to be enumerable to<br>  /// be able to quickly determine (without parsing logs) who can access the pool.<br>  /// @dev The chain selectors are in uint256 format because of the EnumerableSet implementation.<br>  EnumerableSet.UintSet internal s_remoteChainSelectors;<br>  mapping(uint64 remoteChainSelector =&gt; RemoteChainConfig) internal s_remoteChainConfigs;</pre><p>i thought i already set this stuff in the base test. oh looks like we need to set the source chain in the config too. good thing i checked this because we were also setting the peers as allowed pools by mistake</p><p>have not cleaned up unit tests, but have got 100% unit coverage (except for 1 missing branch in a library which after hours of trying, im not going to worry about anymore)</p><h3>7. implement other Yield Strategies</h3><p><a href="https://x.com/Route2FI/status/1929610156314067093">https://x.com/Route2FI/status/1929610156314067093</a></p><p>let’s research how to deposit and withdraw from euler</p><p>euler contract addresses: <a href="https://github.com/euler-xyz/euler-interfaces/tree/master/addresses">https://github.com/euler-xyz/euler-interfaces/tree/master/addresses</a></p><p>ok so looks like according to this page <a href="https://docs.euler.finance/concepts/core/evc/">https://docs.euler.finance/concepts/core/evc/</a></p><p>we want to be interacting with the euler system via that evc contract</p><p>although we’re not going to be borrowing, just depositing usdc to lend. they are using proxies however for a few of these contracts so its probably safer to just go via the main one</p><p>actually i dont think we’ll need that, interacting directly with the vault looks more realistic <a href="https://etherscan.io/address/0xcBC9B61177444A793B85442D3a953B90f6170b7D#writeProxyContract">https://etherscan.io/address/0xcBC9B61177444A793B85442D3a953B90f6170b7D#writeProxyContract</a></p><p>it has a deposit and a mint function. deposit says to consider using mint. we’d need to calculate the mint amount if there’s no getter for whatever amount we’d deposit</p><p>ok questions we need to ask:</p><ol><li>where do we find the contract addresses for all chains for the usdc vault?</li><li>there’s a drop down menu on here: <a href="https://app.euler.finance/?network=base">https://app.euler.finance/?network=base</a> HOWEVER, it doesn’t support all of the chains we were planning.</li><li>how do we deposit usdc/mint shares?</li><li>how do we withdraw?</li></ol><p>I’m beginning to doubt whether integrating euler is worth our finite time for this hackathon. it wont actually showcase more chainlink usage than we are already doing and it takes time and attention away from invariant tests and implementing fees.</p><p>let’s revisit the todo list and figure out our priorities</p><h3>To Do List 3</h3><ol><li>fees</li><li>invariant tests</li><li>certora</li><li>deploy on testnets</li><li>register with time-based automation</li><li>frontend</li><li>comprehensive natspec</li><li>swap contract for swapping any crypto into usdc and then depositing for yieldcoin</li><li>demo video</li><li>detailed README</li><li>clean up unit tests</li><li>clean up peer contracts</li><li>optional: SOLANA CCIP?? depends how much time we have</li><li>optional: implement other strategy protocols such as euler</li><li>submit before 29th</li></ol><h3>Fees</h3><p>let’s think about implementing fees.</p><p>we will take 0.1% or FEE_PERCENT from all deposits.</p><ul><li>calculate fee based on depositAmount shareMintAmount and fee percent</li></ul><p>should we be taking a fee from the deposited usdc or from the minted shares?</p><p>minted shares means we will earn yield on our fees too, rather than leaving collected usdc fees sitting around but the logic could get messy</p><h3>No, invariants</h3><p>no, you know what? let’s do invariants. invariants is smart contract development. fees can come later, and we’ve done fees before. let’s start really getting into the system.</p><p>we’ll need mocks for comet and aave. we can do something simple and just deal these pools extra usdc, but if we’re verifying with the certora prover, then we’d probably want to leave the mocks as simple deposit and withdraw with no interest rate (because we probably dont have time to implement that system)</p><p>hmm mocking aave maybe a little bit trickier with its aToken</p><p>if we’re using the ccip local simulator (not fork because fuzz tests and rpc = no thanks) then we’ll need to use the link token and the mock router from its configuration when deploying our peers</p><p>we also need to call supportNewTokenViaGetCCIPAdmin or one of the other functions that adds a token to its supported mapping with the usdc address we use</p><p>i’m guessing maaaaaybe we dont need to worry about hopping between chains with this local one?</p><p>there’s a big issue with this, in that if this thing has a constant chain selector and our peers are mapped uniquely to each blockchain’s chain selector, then what are we supposed to do? do we use an array or enumerable set of chain selectors in place of its constant?</p><p>Chromion hackathon: doing invariant fuzz runs for local ccip sim fork is not realistic with rpc calls to mainnets. the non-fork ccip sim has a constant chain selector, but we’re mapping peers to unique selectors so</p><p>crosschain invariant testing seems daunting but if you can get 100% coverage with forked mainnets its np. write a handler. it’s as simple as that</p><p>We need to fork CCIP Local sim and instead of having a CONSTANT chain selector, we need to have an array or enumerable set of chain selectors</p><p>looks like we’ll have to fork this import {MockCCIPRouter} from &quot;@chainlink/contracts-ccip/src/v0.8/ccip/test/mocks/MockRouter.sol&quot;;</p><p>which is used in the chainlink-local non-fork ccip sim.</p><p>what’s happening here is the MockRouter from the old chainlink ccip contracts is hardcoding the src chain selector to be sepolia’s when we want it to be dynamic based on which peer is initiating the ccip message</p><p>we may also want to update this in the ccip local sim</p><pre>function isChainSupported(uint64 chainSelector) public pure returns (bool supported) {<br>        supported = chainSelector == CHAIN_SELECTOR;<br>    }</pre><p>but it <em>probably</em> wont have any affect on our invariants. it’s mostly the hardcoding in the mock router we need to fork, so let’s do that.</p><p>this may be tricky because the mockRouter.ccipSend doesn’t take a source chain selector as a param, it takes the dst selector and EVM2AnyMessage, which doesnt include source selector.</p><p>well the sender is abi.encoded in the ccipSend sender: abi.encode(msg.sender),</p><p>so im guessing what we need is a mapping inside the mockRouter of peers ⇒ selectors, and then we just replace the hardcoded sepolia selector with something like peerToSelector[msg.sender]</p><p>This is what I’ve added to the MockRouter:</p><pre>mapping(address peer =&gt; uint64 chainSelector) s_peerToChainSelector;<br>  <br>  sourceChainSelector: s_peerToChainSelector[msg.sender],<br>  <br>  function setPeerToChainSelector(address peer, uint64 chainSelector) external {<br>    s_peerToChainSelector[peer] = chainSelector;<br>  }</pre><p>To keep things, clean, I forked the MockRouter into its own mock file inside chainlink-local, and will replace the MockRouter used in local ccip sim with this new mock.</p><p>getting this revert [Revert] NotEnoughGasForCall()</p><p>it comes from the src/v0.8/shared/call/CallWithExactGas lib in chainlink-local’s chainlink/ccip-contracts dependancy</p><p>hm seems like setting a higher initial gas limit solved it, we need around twice as much in these local tests as we did for the mainnet fork for some reason</p><p>nvm fixed another error with bounded values and the not enough gas one is back</p><p>ok we increased the limit to 600_000 and now getting a erc20 transfer error. that should be easy to fix great getting the not enough gas error again wtf is this</p><p>the problem with this CallWithExactGas lib is most of it is in assembly, making it harder to figure out what is going on</p><p>in the MockRouter, we have:</p><pre>function _routeMessage(<br>        Client.Any2EVMMessage memory message,<br>        uint16 gasForCallExactCheck,<br>        uint256 gasLimit,<br>        address receiver<br>    ) internal returns (bool success, bytes memory retData, uint256 gasUsed) {<br>        // There are three cases in which we skip calling the receiver:<br>        // 1. If the message data is empty AND the gas limit is 0.<br>        //          This indicates a message that only transfers tokens. It is valid to only send tokens to a contract<br>        //          that supports the IAny2EVMMessageReceiver interface, but without this first check we would call the<br>        //          receiver without any gas, which would revert the transaction.<br>        // 2. If the receiver is not a contract.<br>        // 3. If the receiver is a contract but it does not support the IAny2EVMMessageReceiver interface.<br>        //<br>        // The ordering of these checks is important, as the first check is the cheapest to execute.<br>        if (<br>            (message.data.length == 0 &amp;&amp; gasLimit == 0) || receiver.code.length == 0<br>                || !receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId)<br>        ) {<br>            return (true, &quot;&quot;, 0);<br>        }<br>        bytes memory data = abi.encodeWithSelector(IAny2EVMMessageReceiver.ccipReceive.selector, message);<br>        (success, retData, gasUsed) = CallWithExactGas._callWithExactGasSafeReturnData(<br>            data, receiver, gasLimit, gasForCallExactCheck, Internal.MAX_RET_BYTES<br>        );</pre><p>and then in the lib:</p><pre>/// @notice calls target address with exactly gasAmount gas and payload as calldata.<br>  /// Account for gasForCallExactCheck gas that will be used by this function. Will revert<br>  /// if the target is not a contact. Will revert when there is not enough gas to call the<br>  /// target with gasAmount gas.<br>  /// @dev Caps the return data length, which makes it immune to gas bomb attacks.<br>  /// @dev Return data cap logic borrowed from<br>  /// &lt;https://github.com/nomad-xyz/ExcessivelySafeCall/blob/main/src/ExcessivelySafeCall.sol&gt;.<br>  /// @return success whether the call succeeded<br>  /// @return retData the return data from the call, capped at maxReturnBytes bytes<br>  /// @return gasUsed the gas used by the external call. Does not include the overhead of this function.<br>  function _callWithExactGasSafeReturnData(<br>    bytes memory payload,<br>    address target,<br>    uint256 gasLimit,<br>    uint16 gasForCallExactCheck,<br>    uint16 maxReturnBytes</pre><p>well in the mockRouter.ccipSend, the gasLimit being passed to that comes from uint256 gasLimit = _fromBytes(message.extraArgs).gasLimit;</p><p>well i increased the limit to 5 milly and it’s working now</p><p>after an hour or so of setting up an invariant</p><p>WE GET THE NOT ENOUGH GAS ERROR AGAIN AAAAAAAAAAAAAGH THIS</p><p>[obscenities]</p><p>not enough gas with 10m this is ridiculous</p><pre>// if g - g//64 &lt;= gasLimit, revert<br>if iszero(gt(sub(g, div(g, 64)), gasLimit)) {<br>    mstore(0x0, NOT_ENOUGH_GAS_FOR_CALL_SIG)<br>    revert(0x0, 0x4)<br>}</pre><p>this is cursor’s explanation of the assembly:</p><pre>Let me break this down simply:<br>g is the available gas after subtracting gasForCallExactCheck (which is 5,000 gas)<br>The check is: g - (g/64) &lt;= gasLimit<br>This is equivalent to: (63/64) * g &lt;= gasLimit<br>So the revert happens when:<br>The amount of gas you want to use (gasLimit) is greater than 63/64ths of the available gas<br>This is due to EIP-150, which ensures that a contract can&#39;t use more than 63/64ths of the remaining gas in a call. The router is enforcing this limit to prevent gas bombs.<br>For example:<br>If you have 10,000,000 gas available<br>After subtracting gasForCallExactCheck (5,000), you have 9,995,000<br>63/64ths of that is ~9,839,648<br>So if your gasLimit is greater than ~9.84M, it will revert<br>To fix this, you need to set your gasLimit to be less than 63/64ths of the available gas. For 10M gas, that means setting it to around 9.8M or less.</pre><p>this is so stupid</p><p>the ai’s logic here is i set it to something lower to bypass the condition based on the previous setting, as if that wont change the condition itself.</p><p>if i set it to 0 i get this</p><pre>ParentCLF::ccipReceive(Any2EVMMessage({ messageId: 0x6693e7969f9b3792025bb3b11ed0da17093286c4c71d0e97c7e3fe42ea746108, sourceChainSelector: 2, sender: 0x000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a, data: 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000009455ed678000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002, destTokenAmounts: [EVMTokenAmount({ token: 0x40b4863C923385D1632d640B097DDeA6EaB92e76, amount: 39818548856 [3.981e10] })] }))<br>    │   │   │   └─ ← [OutOfGas] EvmError: OutOfGas</pre><p>so we need to send strictly less than the limit we are specifying. how do we find how much we are sending? isnt it going to be a different amount for different tx types……..</p><p>i guess we can just brute force the limit until we find the happy medium between out of gas and not enough gas</p><p>ok its somewhere between 167_000 and 168_000</p><p>this is insane. our mainnet fork tests were fine. i’ve never needed to account for this with ccip before</p><p>169_000 = out of gas</p><p>169_990 = not enough gas</p><p>169_500 = [Revert] panic: array out-of-bounds access (0x32)</p><p>….</p><p><a href="https://www.rareskills.io/post/eip-150-and-the-63-64-rule-for-gas">https://www.rareskills.io/post/eip-150-and-the-63-64-rule-for-gas</a> let’s at least read about this eip before we consider commenting out the gas check</p><p>even if we use 169_500, that still sometimes gives us the not enough gas error. if we comment out the check in the lib, then we get the array out of bounds, so lets debug that</p><p>i’ve commented out the check for now.</p><p>the out of bounds panic error is something to do with small fuzzed shareBurnAmount values that result in 0 usdc being withdrawn from the strategies and then sent and transferred…i think.</p><p>either way we have an issue with small values</p><p>maybe we should just make our share/yieldcoin have 6 decimals too……………</p><p>this is the current withdraw calculation logic: function _calculateWithdrawAmount(uint256 totalValue, uint256 totalShares, uint256 shareBurnAmount) internal pure returns (uint256) { return (shareBurnAmount * totalValue) / totalShares; }</p><p>Precision Issues: If the totalValue and totalShares are very large numbers, and shareBurnAmount is small, the multiplication might overflow or result in a very small number that gets rounded to 0 during division.</p><p>currently, our fuzz tests are making shareBurnAmount a small number and the amount to withdraw is 0 for instance, shareBurnAmount: 19639 totalValue: 1000000 totalShares: 1000000000000000000</p><p>emit ShareBurnUpdate(shareBurnAmount: 347847385716999999999998 [3.478e23], chainSelector: 1, totalShares: 2)</p><p>MockAavePool::balanceOf(ParentCLF: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall] │ │ │ │ │ │ │ └─ ← [Return] 347847385717 [3.478e11]</p><p>theres another potential bug here where if a small amount of shares are left, how will that affect the totalValue?</p><p>so with that shareBurnAmount, we just withdrew 347847385716 from the aave pool, leaving the totalValue at 347847385717 - 347847385716 = 1</p><p>so we have totalShares = 2 and totalValue = 1 in this scenario. when someone new makes a deposit, their shareMintAmount will be (usdcDepositAmount * 2) / 1. That does not look good!</p><p><em>NOTE: THIS SCENARIO WAS HAPPENING WHEN I CONVERTED 0 WITHDRAWAMOUNT TO 1, NOW REMOVED</em> (we should see if this is still the case later)</p><p>it’s a good thing to prioritise invariants because these issues would’ve been difficult to visualise with unit tests</p><p>what happens if someone withdraws the totalValue of 1 and then there is 1 share left?</p><p>maybe we shouldnt be burning any shares until after the correct usdc has been withdrawn</p><p>in an ideal world we would be able to calculate the usdc withdraw amount and revert if 0 based on shareBurnAmount. this would require multiple ccip txs just to read state</p><p>ok we need to seriously weigh up our options before continuing.</p><p>do we implement extra ccip tx types for fetching usdc withdraw amount before burning (or otherwise sending back to withdrawer) shares? basically the withdrawer would be sending a request to burn</p><p>im thinking you know what, if they send a small amount, then whatever, give em 0 back lmao</p><p>if _calculateWithdrawAmount returns 0, then…. just return? hehehehehehehe. i get there should be some protection for fatfingering idiots but is it really worth the extra logic of sending multiple ccip txs so someone can withdraw tiny fractions of a cent? This line of thinking is entirely due to the time constraints of the hackathon btw. if this was a legitimate project that would be deployed, this issue would be given serious and significant consideration on how to make it as fair as possible. the actual production answer would likely be to use concero.</p><p>i dunno im not so satisfied with this. a min shareburnamount check would be ideal but ccip [REDACTED]</p><p>I think this should be marked down as a <strong>KNOWN ISSUE: BURNING SMALL AMOUNTS OF SHARES RESULTS IN 0 USDC WITHDRAWN</strong> for now</p><p>that is really annoying. is there really nothing we can do to rectify this? using some solady math lib and reducing the decimals per share?</p><p>Let’s just properly document this known issue for now</p><h3>Known Issue: Burning small amounts of shares can result in 0 usdc withdrawn</h3><p>It can take up to 3 CCIP txs to calculate the usdcWithdrawAmount based on the shareBurnAmount</p><p>Consider this scenario:</p><ul><li>shareBurnAmountwithdrawal initiated on child1</li><li>totalShares is on parent</li><li>totalValue is on child2 (strategy)</li></ul><p>These 3 values are required to calculate the usdcWithdrawAmount. When the shareBurnAmount is significantly small, the calculation can return less than the minimum amount of USDC to 6 decimals $1.000000 (.000000). This value is a significantly small fraction of a cent. If we tried to calculate this amount, it would return 0. Then if we try to withdraw this amount (0) from a yield strategy and send it across ccip to be transferred to the withdrawer, it would cause issues such as the transfer failing because the transferAmount is 0.</p><p>Therefore the current, unfortunate solution (at least for the time constrained context of the hackathon) is to not withdraw or transfer anything if someone burns an amount of shares small enough that it is worth less than the 6th decimal after a $.</p><p>It is unlikely to expect anyone to want to withdraw such a small amount of dust, but this edge case still needs to be acknowledged, and is handled as such by simply allowing the small amount of shares to be burnt without any redemption of USDC.</p><p>We could add a min burn amount, and revert if this amount is not provided when a burn is attempted, but to do so would require the same above CCIP txs + extra ones just to tell the withdrawer that no, we cant burn your piddly amount of shares.</p><h3>Back to invariants (oh yeah)</h3><p>now we dont have 100% coverage on the YieldPeer</p><p>| src/peers/YieldPeer.sol | 98.55% (136/138) | 99.17% (120/121) | 100.00% (18/18) | 97.73% (43/44) |</p><p>annoying</p><p>oh that lack of coverage was just cus of a getter we added for the strategy pool. easypeasy</p><p>emit ShareMintUpdate(shareMintAmount: 0, chainSelector: 1, totalShares: 1)</p><p>this isnt very nice. so it looks like in the fuzz runs we’ve ended up with 1 totalShare which is likely contributing to the shareMintAmount being 0. i wonder what totalValue is in this scenario</p><p>looks like 484862598387</p><p>depositAmount: 194507369801</p><p>shareMintAmount = (194507369801 * 1) / 484862598387;</p><p>equals 0.401159772785261, so solidity is rounding it 0 of course.</p><p>this is a bad bug. how on earth do we handle crosschain precision? we can’t prevent someone with a big amount of shares burning all of them except for small amounts of wei. might have to spend the evening reading solodit</p><p>maybe if we have an admin mint the initial shares and then never withdraw, that could provide some sort of mitigation</p><p>ok that seems to be providing some mitigation. let’s increase the fuzz runs, see if we find anything weird. seems good.</p><h3>Known issue: Share minting inflation attack</h3><p>Mitigation: initial admin mint</p><h3>Solana CCIP</h3><p><a href="https://app.kamino.finance/earn/lend">https://app.kamino.finance/earn/lend</a> this looks like an option for lending usdc on solana</p><h3>Back to invariants/known issues</h3><p><a href="https://solodit.cyfrin.io/issues/using-atokenbalanceof-in-claggaaveadapter_compoundaccounting-allows-for-inflation-at-tacks-cantina-none-clave-pdf">https://solodit.cyfrin.io/issues/using-atokenbalanceof-in-claggaaveadapter_compoundaccounting-allows-for-inflation-at-tacks-cantina-none-clave-pdf</a></p><p>this looks relevant</p><p>invariants are going very well. if i start certora now i’ll be about a week ahead of schedule. it’d be amazing if i have enough time to learn enough rust to implement ccip on solana and move usdc in and out of a lending protocol there, like that kamino thing</p><p><a href="https://docs.chain.link/ccip/directory/testnet/token/USDC">https://docs.chain.link/ccip/directory/testnet/token/USDC</a> its a shame the solana testnet isnt on here. at least i dont have to worry about rushing to learn rust. good thing i checked this before spending a week trying to implement it</p><p>these invariants are looking pretty good. it’s been surprisingly simple and straight forward, this handler. im not sure if its because i’ve been doing invariants so much lately or because the external functions to handle are so few and simple. i expected the crosschain stuff to make this harder, but so much of it is abstracted away, it’s been easy to focus on overall system invariants</p><p>maybe lets get started with C E R T O R A</p><h3>Certora</h3><p>the most fun part of smart contract development is certora</p><p>ok i needed to make a new mock ccip router for certora. we’re not simulating crosschain like in the foundry invariants, just verifying functionality for each contract, so this is a much simpler mock router than the one that i forked from chainlink-local</p><p>im doing basic certora rules and the prover is giving timeouts which is not good. i suspect it has something to do with the libraries as i havent used certora much with custom libraries that interact with external protocols. i’ve linked all the correct immutables and storage contracts in the scene.</p><p>this command made everything pass after 13 mins:</p><pre>certoraRun ./certora/conf/Parent.conf --prover_args &#39;-summarizeExtLibraryCallsAsNonDetPreLinking true&#39; --nondet_difficult_funcs</pre><p>let’s try it like this certoraRun ./certora/conf/Parent.conf --nondet_difficult_funcs</p><p>and we’ll also try it without the flag just to double check the timeouts from earlier werent being caused by incorrect links in the scene.</p><p>whilst waiting for those lets have a look again at what --nondet_difficult_funcs is doing</p><p><a href="https://docs.certora.com/en/latest/docs/prover/cli/options.html#nondet-difficult-funcs">https://docs.certora.com/en/latest/docs/prover/cli/options.html#nondet-difficult-funcs</a></p><p>ok so basically it just summarizes unsummarized complicated view functions</p><p>right so the reason we need that is because the CLF FunctionsRequest contains functions that do a lot with strings and bytes and seeing as strings and bytes can have a LOT of different possible values, the prover tries to explore all these possible paths, and for the purpose of our testing, we dont care about verifying a chainlink library because we already know chainlink code is good. we want to verify our own code.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/802/1*vJRAGMbT5TuCdQkHqmoQGQ.png" /></figure><p>that was probably the best thing i’ve learnt so far from this project.</p><p>it makes sense to verify the parent peer separately from the clf extension</p><p>im trying to verify the YieldPeer separately so I don’t end up copy and pasting all of the same logic between the Parent and Child specs. I made a YieldHarness and am verifying ccipReceive via that. also verified the internal functions for handling ccip tx, basic deposits and withdraws, as well as the decoding for withdrawal chain selector being an allowed chain</p><p>what about a Yield.spec and there are 2 confs pointing to it, each with child and parent. then the readme would just include the two commands to run each conf, checking the same rules and invariants for all peers. this would require the rules in this mutual spec to not contain specific helpers, unless both the childharness and parentharness inherited the same helper harness. this is making sense.</p><p>let’s figure out what files we need:</p><ul><li>Yield.spec (specifies behaviour of the abstract YieldPeer contract, in particular exposed internal functions)</li><li>Yield.conf</li><li>ParentCLF.spec (chainlink functions/automation)</li><li>ParentCLF.conf</li><li>Parent.spec (behaviour specific to ParentPeer)</li><li>Parent.conf</li><li>Child.spec (behaviour specific to ChildPeer)</li><li>Child.conf</li><li>BasePeer.spec (behaviour shared by Parent and Child)</li><li>BaseParent.conf (for pointing ParentPeer.sol to BasePeer.spec)</li><li>BaseChild.conf (for pointing ChildPeer.sol to BasePeer.spec)</li></ul><p>consider this</p><p><strong><em>Feature idea</em></strong><em> </em><strong><em>DEPOSITS SHOULD HAVE THE OPTION TO SPECIFY WHICH CHAIN TO RECEIVE MINTED SHARES ON, SIMILAR TO HOW WITHDRAW FEATURES THAT</em></strong></p><p>the contract level yield infrastructure is a crosschain network of contracts that facilitate usdc deposits and withdraws in exchange for minting and burning yieldcoin</p><h3>To Do List 4</h3><ol><li>invariant tests</li><li>certora</li><li>deploy on testnets</li><li>register with time-based automation</li><li>get testnet link and eth on each chain</li><li>set crosschain vars including local peer for each chain</li><li>frontend</li><li>comprehensive natspec</li><li>demo video</li><li>diagrams</li><li>different tx types and scenarios flow</li><li>detailed README</li><li>fees (optional)</li><li>optional: swap contract for swapping any crypto into usdc and then depositing for yieldcoin</li><li>run slither and aderyn</li><li>double check for unused code such as old definition events in certora</li><li>clean up unit tests</li><li>clean up peer contracts</li><li>optional: implement other strategy protocols such as euler</li><li>submit before 29th</li></ol><pre>invariant strategyPool_consistency()<br>    getStrategy().chainSelector != getThisChainSelector() =&gt;<br>	  getStrategyPool() == 0;</pre><p>this is failing. why?</p><p><strong>ParentHarness.getThisChainSelector() ↪ 0xe34</strong></p><p>(ParentHarness.getStrategy()).chainSelector != ParentHarness.getThisChainSelector() =&gt; ParentHarness.getStrategyPool() == 0 ↪</p><p><strong>false</strong></p><p>(ParentHarness.getStrategy()).chainSelector != ParentHarness.getThisChainSelector() ↪</p><p><strong>true</strong></p><p>(ParentHarness.getStrategy()).chainSelector == ParentHarness.getThisChainSelector() ↪</p><p><strong>false</strong></p><p>(ParentHarness.getStrategy()).chainSelector ↪</p><p><strong>0x4305</strong></p><p>ParentHarness.getThisChainSelector() ↪</p><p><strong>0xe34</strong></p><p>ParentHarness.getStrategyPool() == 0 ↪</p><p><strong>false</strong></p><p><strong>ParentHarness.getStrategyPool() ↪ 0x10</strong></p><p>YieldPeer._handleCCIPRebalanceNewStrategy(data=bytes)</p><p>Evaluate branch condition</p><p>Declare newStrategy</p><p><em>keyboard_arrow_down</em></p><p>YieldPeer._updateStrategyPool(chainSelector=</p><p><strong>0xe34</strong></p><p>, protocol=</p><p><strong>IYieldPeer.Protocol.Compound</strong></p><p>) ↪</p><p><strong>MockComet</strong></p><p>Evaluate branch condition</p><p><em>keyboard_arrow_down</em></p><p>YieldPeer._getStrategyPoolFromProtocol(protocol=</p><p><strong>IYieldPeer.Protocol.Compound</strong></p><p>) ↪</p><p><strong>MockComet</strong></p><p><em>chevron_right</em></p><p>YieldPeer._getProtocolConfig() ↪ ProtocolOperations.ProtocolConfig</p><p><em>chevron_right</em></p><p>Global State #4</p><p><em>chevron_right</em></p><p>ProtocolOperations.getStrategyPoolFromProtocol(protocol=</p><p><strong>IYieldPeer.Protocol.Compound</strong></p><p>, config={usdc=</p><p><strong>MockUsdc</strong></p><p>, aavePoolAddressesProvider=</p><p><strong>MockPoolAddressesProvider</strong></p><p>, comet=</p><p><strong>MockComet</strong></p><p>}) ↪</p><p><strong>MockComet</strong></p><p>Evaluate branch condition</p><p>Evaluate branch condition</p><p>strategyPool ↪</p><p><strong>0x10</strong></p><p><em>chevron_right</em></p><p>Global State #5</p><p>Store at ParentHarness.s_strategyPool ↪</p><p><strong>MockComet</strong></p><p><em>chevron_right</em></p><p>Global State #6</p><p><strong>newStrategyPool ↪ 0x10</strong></p><p>it looks like ccipReceive is calling _handleRebalanceNewStrategy which calls this</p><pre>/// @notice Internal helper to handle strategy pool updates<br>    /// @param chainSelector The chain selector for the strategy<br>    /// @param protocol The protocol for the strategy<br>    /// @return strategyPool The new strategy pool address<br>    function _updateStrategyPool(uint64 chainSelector, Protocol protocol) internal returns (address strategyPool) {<br>        if (chainSelector == i_thisChainSelector) {<br>            strategyPool = _getStrategyPoolFromProtocol(protocol);<br>            s_strategyPool = strategyPool;<br>        } else {<br>            s_strategyPool = address(0);<br>        }<br>        emit StrategyPoolUpdated(strategyPool);<br>    }</pre><p>but the chainSelectors are different, as confirmed here</p><p>(ParentHarness.getStrategy()).chainSelector ↪</p><p><strong>0x4305</strong></p><p>ParentHarness.getThisChainSelector() ↪</p><p><strong>0xe34</strong></p><p>but look, the parent selector is getting passed to that function</p><p><strong>YieldPeer._updateStrategyPool(chainSelector=0xe34, protocol=IYieldPeer.Protocol.Compound</strong></p><p>well it looks like we have found a legitimate issue with our code, thanks to certora. the problem is we are updating the strategyPool twice in the scenario the old strategy is a child and the new strategy is a parent. this is the flow:</p><ol><li>CLF fulfillRequest callback sets the new strategy on parent</li><li>This includes `_updateStrategyPool</li></ol><p>hang on wait, nope, that’s not true. we are not updating the strategy pool on the parent during the clf callback</p><p>what’s happening is we are updating the s_strategy struct that contains the strategy chain selector and protocolEnum</p><p>and then updating the pool in the rebalance from old (child) to new (parent) callback</p><p>so basically the certora invariant is failing because it doesnt “know” the crosschain calltrace, and is assigning a random value to s_strategy.chainSelector so when the ccipReceive gets called, there is no prior context for the s_strategy being set during the clf callback, and then triggering the subsequent ccip txs that would result in this ccipReceive simulation. this is a good example of why it was useful to also test this invariant in foundry with the local ccip sim, and demonstrates the limitations of either certora or my own ability to use it</p><p>Trying to verify that parent.deposit mints shares. but it’s failing.</p><pre>rule deposit_mintsShares_when_parent_is_strategy() {<br>    env e;<br>    calldataarg args;<br>    require getStrategy().chainSelector == getThisChainSelector();<br>    <br>    uint256 shareSupplyBefore = share.totalSupply();<br>    uint256 totalSharesBefore = getTotalShares();<br>    deposit(e, args);<br>    assert share.totalSupply() &gt; shareSupplyBefore;<br>    assert getTotalShares() &gt; totalSharesBefore;<br>}</pre><p><strong>ParentHarness.deposit(amountToDeposit=0x1)</strong></p><p><strong>YieldPeer._getTotalValueFromStrategy(strategyPool=MockComet) ↪ 0x2</strong></p><p><strong>ProtocolOperations._getTotalValueFromCompound(comet=MockComet) ↪ 0x2</strong></p><p>so we’re deposting 0x1 worth of USDC and the totalValue in the system is 0x2</p><p>i think this is another interpretation of the inflation attack vulnerability we already uncovered with the foundry invariant tests and can likely be mitigated by requiring the totalValue to be &gt; INITIAL_ADMIN_DEPOSIT</p><p>ok even when we require that, it still calculates a mint amount as 0</p><p>ParentPeer._calculateMintAmount(totalValue=</p><p><strong>0xfff…270</strong></p><p>, amount=</p><p><strong>0x1</strong></p><p>) ↪</p><p><strong>0x0</strong></p><p>Load from ParentHarness.s_totalShares ↪</p><p><strong>0xfff…26f</strong></p><p><strong>totalShares ↪ 0xfff…26f</strong></p><p>totalValue = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe270</p><p>totalShares = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe26f</p><p>usdcDepositAmount = 0x1</p><p>and this results in 0 minted shares, which is bad.</p><p>perhaps we should have a minimum 1usdc deposit amount</p><p>let’s try this</p><pre>// _revertIfZeroAmount(amountToDeposit);<br> if (amountToDeposit &lt; 1e6) revert YieldPeer__NoZeroAmount();</pre><p>even when doing that, calculateMintAmount is still returning 0. This is bad.</p><p><strong>ParentPeer._calculateMintAmount(totalValue=0xde0…241, amount=0xf4240) ↪ 0x0</strong></p><p><strong>Load from ParentHarness.s_totalShares ↪ 0xe8d4a51001</strong></p><p>well this is looking like a critical severity bug</p><p>totalValue: 0xde0b6b3a7734241</p><p>totalShares: 0xe8d4a51001</p><p>usdcDepositAmount: 0xf4240</p><p>these values return a 0 shareMintAmount. Which is very bad because we clearly have value and shares in the system and the depositor is providing a minimum of 1 usdc and yet is getting nothing in return. this is why we need to do formal verification — to catch edgecases like this.</p><p>HOW does this get fixed</p><p>i tried implementing solady’s fixedPointMathLib.fullMulDivUp and got a very quick certora timeout, something i havent seen before. <a href="https://docs.certora.com/en/latest/docs/user-guide/out-of-resources/timeout.html#high-nonlinear-op-count">https://docs.certora.com/en/latest/docs/user-guide/out-of-resources/timeout.html#high-nonlinear-op-count</a></p><p>“Nonlinear integer arithmetic is often the hardest part of the formulas that the Certora Prover is solving.” great.</p><p>Chromion hackathon: mitigating precision loss for a system that accepts and mints crosschain deposits and shares is trickier than simply requiring minimum amounts and enforcing an initial admin deposit.</p><p>remember to remove solady if not end up using it</p><p>right i’ll come back to deposit_mintsShares_when_parent_is_strategy - this is a serious critical edgecase uncovered, but i’ve got a ton of other stuff to get through today so im going to verify as much other stuff for the parent peer as possible first</p><p>reading precision loss findings on solodit whilst waiting for certora runs. this looks relevant: <a href="https://solodit.cyfrin.io/issues/m-01-the-value-of-lp-token-can-be-manipulated-by-the-first-minister-which-allows-the-attacker-to-dilute-future-liquidity-providers-shares-code4rena-elasticswap-elasticswap-git">https://solodit.cyfrin.io/issues/m-01-the-value-of-lp-token-can-be-manipulated-by-the-first-minister-which-allows-the-attacker-to-dilute-future-liquidity-providers-shares-code4rena-elasticswap-elasticswap-git</a> it sounds kindve similar to what i was already thinking with the initial admin mint. this suggests minting some lp (share) tokens to a locked address.</p><p>i think i just found another bug with certora. in _setStrategy (called by clf callback), the strategy is updated, right… and if the old strategy is on the parent chain, but the new strategy isn’t, it calls _handleStrategyMoveToNewChain</p><p>which gets the old strategyPool like so address oldStrategyPool = _getStrategyPool(), which returns s_strategyPool</p><p>nope nevermind, its just certora being weird.</p><p>im explicitly requiring oldStrategy.protocol != protocol; in the certora rule, and yet it’s withdrawing and depositing into the same pool</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8UKz_7e_wfizdquTmVHyhw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3xrcCyQIRGOC3wpbyTBeFQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/1*hbSTPrJ2CvKbFXYzKbwu2w.png" /></figure><p>so why is it withdrawing from compound if the old strategy is aave? im thinking certora is assigning the s_strategyPool address to something it shouldnt be so maybe lets constrain that</p><p>lets see if it works with these lines</p><pre>require oldStrategy.protocol == IYieldPeer.Protocol.Aave =&gt; currentContract.s_strategyPool == getAave();<br>    require oldStrategy.protocol == IYieldPeer.Protocol.Compound =&gt; currentContract.s_strategyPool == getCompound();</pre><p>ok it worked.</p><p>just gonna leave this comment here:</p><pre>// @review this is failing - doesnt &quot;know&quot; the crosschain calltrace<br>// this is breaking because we update the strategyPool if it&#39;s a RebalanceNewStrategy callback to the Parent from the old strategy Child<br>// CLF updates s_strategy on parent -&gt; parent.rebalanceOldStrategy to old strategy child -&gt; child.rebalanceNewStrategy back to parent, updating s_stratgyPool<br>invariant strategyPool_consistency()<br>    getStrategy().chainSelector != getThisChainSelector() =&gt; getStrategyPool() == 0;</pre><p>i finished verifying ParentPeer, mostly. everything works as expected except for that edge case. i think once done with certora i should dedicate an entire day at least to this issue before doing the frontend</p><h3>Important:</h3><p>src.js/src.min.js will need to be updated with testnet selectors and whatever else that requires</p><p>i just noticed something that’s unfortunate for testnet deployments — aave v3 is only on eth sepolia, and cUSDCv3 (compound) is only on eth sepolia, base sepolia, and polygon mumbai</p><p>oh i hope the defi llama api works with testnets. better check that now. can always just use the mainnet values i suppose and acknowledge it wont be strictly correct, but just demonstrates the chainlink functionality</p><p>pretty annoyed this didnt occur to me earlier</p><p>let’s revisit todo list</p><h3>To Do List 5</h3><ol><li>invariant tests</li><li>certora</li><li>precision loss issue</li><li>deploy on testnets</li><li>register with time-based automation</li><li>get testnet link and eth on each chain</li><li>set crosschain vars including local peer for each chain</li><li>frontend</li><li>comprehensive natspec</li><li>demo video</li><li>diagrams</li><li>different tx types and scenarios flow</li><li>detailed README</li><li>fees (optional)</li><li>optional: swap contract for swapping any crypto into usdc and then depositing for yieldcoin</li><li>run slither and aderyn</li><li>double check for unused code such as old definition events in certora</li><li>clean up unit tests</li><li>clean up peer contracts</li><li>optional: implement other strategy protocols such as euler</li><li>submit before 29th</li></ol><p>im kinda burnt out from all the certora today. going to do some natspec</p><p>its all well and good copy and pasting this todo list. i really need to think and prioritise what needs to happen next.</p><ol><li>precision loss bug</li><li>testnet deployments</li><li>defi llama api doesnt provide testnet data so the option im thinking is:</li><li>use mainnet apy values</li><li>acknowledge this unfortunate reality</li><li>demonstrate CLY system’s chainlink functionality</li><li>what chains?</li><li>eth sepolia</li><li>base sepolia</li><li>polygon mumbai</li><li>register with time-based automation on parent chain</li><li>get testnet link and eth on each chain</li><li>set crosschain vars including local peer for each chain</li><li>compound is on all those testnets, aave is only on eth. i can always deploy a mock aave contract that doesnt generate any yield for base and polygon…….</li></ol><p>I think once these things are done, then I should get a frontend done asap, along with video, diagrams etc and basically have a “finished” submission so i dont have to stress out for the last week trying to do it. then spend the last week polishing my tests, maybe implement fees. something like that.</p><p>OH NICE ONE, COMPOUND. polygon mumbai isnt even the active testnet anymore, it’s amoy. i thought mumbai was old.</p><p>well i need at least 3 testnets. eth sepolia and base sepolia it is. im going to need to deploy mocks for both aave and compound on 3rd chain</p><ul><li>eth sepolia</li><li>aave — Yes</li><li>compound — Yes</li><li>base sepolia</li><li>aave — No — mock it</li><li>compound — Yes</li><li>chain c</li><li>aave — No — mock it</li><li>compound — No — mock it</li></ul><p>obviously the first thing to consider here is that we wont be generating any actual yield in our testnet strategy mocks BECAUSE we are not actually lending money out to any active markets and dont have enough testnet usdc. but thats ok, we can just deposit and withdraw the usdc we do have without any yield, it wont change anything about our system’s functionality</p><p>chain c will probably be optimism because i like the name, better than pessimism. could do avalanche seeing as they’re a sponsor…….. maybe its better off doing it with avalanche. i doubt they’d pick this project as a winner because its not avalanche centric. could even make avalanche the parent chain hehe</p><p>ok so next priority is make more mocks for aave and compound</p><p>Chromion hackathon: the (brief) invariant testing and formal verification only really highlighted one issue with precision loss. As this could end up being a long rabbithole, the current priority is to get a suitable submission done as quickly as possible, then spend the rest of the hackathon on the vulnerability.</p><p>we need to revisit this <strong>handleCCIPWithdrawToStrategy_withdrawsFromStrategy for ChildHarness</strong></p><p>next steps:</p><ol><li>unit test mock aave and compound (no yield) contracts</li><li>deploy them on relevant chains</li><li>get testnet eth and link</li><li>peers must be funded with link</li><li>peers must set allowed chains and peers (and their local selves)</li></ol><h3>Deploying testnet mocks</h3><p>avalanche needs both the aave and compound</p><p>base just needs aave</p><p><a href="https://solodit.cyfrin.io/issues/using-atokenbalanceof-in-claggaaveadapter_compoundaccounting-allows-for-inflation-at-tacks-cantina-none-clave-pdf">https://solodit.cyfrin.io/issues/using-atokenbalanceof-in-claggaaveadapter_compoundaccounting-allows-for-inflation-at-tacks-cantina-none-clave-pdf</a></p><p><a href="https://x.com/getreconxyz/status/1933177653961326623">https://x.com/getreconxyz/status/1933177653961326623</a> leaving these here for later</p><p>alright going with avalanche fuji as the parent chain despite neither of the actual strategy contracts being deployed there. only cus they sponsoring, and you never know might get lucky with a prize.</p><p>will we need separate api keys for verifying contracts on snowtrace or whatever the avax explorer is or is etherscan now doing that thing where only one is needed overall?</p><p>forge script script/deploy/mocks/DeployMockCometNoYield.s.sol:DeployMockCometNoYield --rpc-url $AVALANCHE_FUJI_RPC_URL --account myKeystore --broadcast</p><p>i set access control on the withdraw funcs so nobody (unlikely to happen but just in case) withdraws our test usdc from an active strategy</p><p>so will need to remember to call setPeer on the mock pools after deploying the peer contract</p><p><a href="https://testnet.snowtrace.io/verifycontract">https://testnet.snowtrace.io/verifycontract</a> I. HATE. VERIFYING. ON. BLOCK. EXPLORERS.</p><p>ok compound mock is verified, but that was an easy one cus no constructor args <a href="https://testnet.snowtrace.io/address/0x43a5Ddb9561762D835B6c0f15Cb8a7ed02F6D958/contract/43113/code">https://testnet.snowtrace.io/address/0x43a5Ddb9561762D835B6c0f15Cb8a7ed02F6D958/contract/43113/code</a></p><p>forge script script/deploy/mocks/DeployMockAaveNoYield.s.sol --rpc-url $AVALANCHE_FUJI_RPC_URL --account myKeystore --broadcast</p><p>0: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 1: contract MockAaveNoYield 0x546cb5Df0f97CD19b1242fa8cE24e51dA8665f7F 2: contract MockAToken 0x04c3339a6C72Cd783AC3E5C5ea8544D90e458A64</p><p>oh wait i forgot the aave addresses provider mock. eurhgghg</p><p>lets try that again</p><p>0: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 1: contract MockAaveNoYield 0xc05be4dAf4DB98d061Dd769B685CA7EEF82Eb767 2: contract MockAToken 0x62926eE0AaC06c5378f7eDD2361393f10eC39B5b 3: contract MockPoolAddressesProvider 0xEA8176c28417A6FD2F2e40F871EA04dfFE1CAcd8</p><p>i hate flattening files and verifying manually. why cant it just work from the command line everytime</p><p>the stuff i already set up with chainlink functions and base sepolia may not work with avalanche. i dont know for definite but the secrets thing might be affected by different don ids or whatever. if it doesnt work, i’ll just use base as parent</p><p>why does snowtrace fuji not have the option to verify with an api key? am i just being stupid? do they really expect people to want to deploy on this chain when they cant verify on block explorers easily</p><p>fine i’ll just flatten everything and [REDACTED]</p><p>oh wow i verified and i didnt need to do ANYTHING with constructor args. i take it back, the verification on snowtrace fuji is actually decent. still a pain having to flatten and then drag and drop the file</p><p><a href="https://testnet.snowtrace.io/address/0xEA8176c28417A6FD2F2e40F871EA04dfFE1CAcd8/contract/43113/code">~~https://testnet.snowtrace.io/address/0xEA8176c28417A6FD2F2e40F871EA04dfFE1CAcd8/contract/43113/code</a> addresses provider~~</p><p><a href="https://testnet.snowtrace.io/address/0x62926eE0AaC06c5378f7eDD2361393f10eC39B5b/contract/43113/code">~~https://testnet.snowtrace.io/address/0x62926eE0AaC06c5378f7eDD2361393f10eC39B5b/contract/43113/code</a> aToken~~</p><p>i am legit very impressed at the verification on snowtrace testnet. no issues whatsoever so far.</p><p><a href="https://testnet.snowtrace.io/address/0xc05be4dAf4DB98d061Dd769B685CA7EEF82Eb767/contract/43113/code">~~https://testnet.snowtrace.io/address/0xc05be4dAf4DB98d061Dd769B685CA7EEF82Eb767/contract/43113/code</a> aavePool~~ ← this one needs to setPeer</p><p>could write a script for setting peers, but will probably just use cast send tbh</p><p>ok we need to deploy aave on base</p><p>forge script script/deploy/mocks/DeployMockAaveNoYield.s.sol --rpc-url $BASE_SEPOLIA_RPC_URL --account myKeystore --broadcast</p><p>0: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 1: contract MockAaveNoYield 0x3C1A3FD4F5eb59c2746BA2BaFB10C4Ef8b745787 2: contract MockAToken 0x6fDbc908533d16983E3c0d91027eA78fd2A84E9B 3: contract MockPoolAddressesProvider 0xC88543241BE4A46Ca5AFC2d44100A0cc767D6B78</p><p>should probably double check im using the right usdc address for these. yes i am, but good to make sure.</p><pre>forge verify-contract \\<br> --chain-id 84532 \\<br> --etherscan-api-key $BASESCAN_API_KEY \\<br> --rpc-url $BASE_SEPOLIA_RPC_URL \\<br> 0xC88543241BE4A46Ca5AFC2d44100A0cc767D6B78 \\<br> ./test/mocks/MockPoolAddressesProvider.sol:MockPoolAddressesProvider \\<br> --constructor-args $(cast abi-encode &quot;constructor(address)&quot; &quot;0x3C1A3FD4F5eb59c2746BA2BaFB10C4Ef8b745787&quot;) \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>much more dignified than flattening.</p><pre>forge verify-contract \\<br> --chain-id 84532 \\<br> --etherscan-api-key $BASESCAN_API_KEY \\<br> --rpc-url $BASE_SEPOLIA_RPC_URL \\<br> 0x6fDbc908533d16983E3c0d91027eA78fd2A84E9B \\<br> ./test/mocks/MockAToken.sol:MockAToken \\<br> --constructor-args $(cast abi-encode &quot;constructor(address)&quot; &quot;0x3C1A3FD4F5eb59c2746BA2BaFB10C4Ef8b745787&quot;) \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><pre>forge verify-contract \\<br> --chain-id 84532 \\<br> --etherscan-api-key $BASESCAN_API_KEY \\<br> --rpc-url $BASE_SEPOLIA_RPC_URL \\<br> 0x3C1A3FD4F5eb59c2746BA2BaFB10C4Ef8b745787 \\<br> ./test/mocks/testnet/MockAaveNoYield.sol:MockAaveNoYield \\<br> --constructor-args $(cast abi-encode &quot;constructor(address)&quot; &quot;0x036CbD53842c5426634e7929541eC2318f3dCF7e&quot;) \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>ok we’ve deployed and verified our mocks. lovely.</p><p>aavepool: <a href="https://sepolia.basescan.org/address/0x3c1a3fd4f5eb59c2746ba2bafb10c4ef8b745787#code">https://sepolia.basescan.org/address/0x3c1a3fd4f5eb59c2746ba2bafb10c4ef8b745787#code</a></p><p>aToken: <a href="https://sepolia.basescan.org/address/0x6fdbc908533d16983e3c0d91027ea78fd2a84e9b#code">https://sepolia.basescan.org/address/0x6fdbc908533d16983e3c0d91027ea78fd2a84e9b#code</a></p><p>addressProvider: <a href="https://sepolia.basescan.org/address/0xc88543241be4a46ca5afc2d44100a0cc767d6b78#code">https://sepolia.basescan.org/address/0xc88543241be4a46ca5afc2d44100a0cc767d6b78#code</a></p><p>The next step is to deploy peer contracts</p><h3>Deploy Peers on testnets</h3><ol><li>deploy child peer on eth sepolia</li><li>deploy child peer on base sepolia</li><li>setPeer on mock aave pool</li><li>deploy parentCLF on avalanche fuji</li><li>setPeer on mock aave pool</li><li>setPeer on mock compound pool</li><li>add clf consumer</li><li>set allowed chains (including local) on all chains</li><li>set allowed peers (including local) on all chains</li><li>change src.min.js to use testnet selectors</li></ol><p>if CLF doesnt work with fuji,</p><ol><li>deploy parent CLF on base sep</li><li>setPeer on mock aave</li><li>add clf consumer</li><li>deploy child on avafuji</li><li>setpeer aave</li><li>setpeer compound</li></ol><h3>Deploy child peer on eth sepolia</h3><p>forge script script/deploy/DeployChild.s.sol --rpc-url $ETH_SEPOLIA_RPC_URL --account myKeystore --broadcast</p><p>0: contract Share 0x4Ff47A26E792E651e8F7AA055D918901fC9EdDc5 1: contract SharePool 0x6DE0DC29B6de3BEC203383c2d614c61750848BAf 2: contract ChildPeer 0xAF168497dFE7DC261881dd698159a814564ccd7c 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p>oh right the shares.</p><p>peers need to be funded with link too.</p><pre>forge verify-contract \\<br> --chain-id 11155111 \\<br> --etherscan-api-key $ETHERSCAN_API_KEY \\<br> --rpc-url $ETH_SEPOLIA_RPC_URL \\<br> 0x4Ff47A26E792E651e8F7AA055D918901fC9EdDc5 \\<br> ./src/token/Share.sol:Share \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>~~eth sep share: <a href="https://sepolia.etherscan.io/address/0x4ff47a26e792e651e8f7aa055d918901fc9eddc5#code~~">https://sepolia.etherscan.io/address/0x4ff47a26e792e651e8f7aa055d918901fc9eddc5#code~~</a></p><pre>forge verify-contract \\<br> --chain-id 11155111 \\<br> --etherscan-api-key $ETHERSCAN_API_KEY \\<br> --rpc-url $ETH_SEPOLIA_RPC_URL \\<br> 0x6DE0DC29B6de3BEC203383c2d614c61750848BAf \\<br> ./src/token/SharePool.sol:SharePool \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>why is this not verifying now</p><p>seriously why is this not working</p><p>oh its missing constructor args duh</p><pre>forge verify-contract \\<br> --chain-id 11155111 \\<br> --etherscan-api-key $ETHERSCAN_API_KEY \\<br> --rpc-url $ETH_SEPOLIA_RPC_URL \\<br> 0x19aeA7614a7f2BAb5133F4cb7E0977a04e510677 \\<br> ./src/token/SharePool.sol:SharePool \\<br> --constructor-args $(cast abi-encode &quot;constructor(address,address,address)&quot; &quot;0xcE287EAB90E809Af9a6d6288c9196e7eB4Fd9584&quot; &quot;0xba3f6251de62dED61Ff98590cB2fDf6871FbB991&quot; &quot;0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59&quot;) \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>still not working. possibly because the first param is of type IBurnMintERC677 and not actually an address….</p><p>i changed the sharePool. just gonna run deploy script again so i dont have to call the grantMintBurnRoles again</p><p>0: contract Share 0xcE287EAB90E809Af9a6d6288c9196e7eB4Fd9584 1: contract SharePool 0x19aeA7614a7f2BAb5133F4cb7E0977a04e510677 2: contract ChildPeer 0x3CaD05D15B436A4542660e596B59FAC8EB4B38Bd 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p>~~eth share: <a href="https://sepolia.etherscan.io/address/0xcE287EAB90E809Af9a6d6288c9196e7eB4Fd9584#code~~">https://sepolia.etherscan.io/address/0xcE287EAB90E809Af9a6d6288c9196e7eB4Fd9584#code~~</a></p><p>~~eth sharePool (NOW its verifying): <a href="https://sepolia.etherscan.io/address/0x19aea7614a7f2bab5133f4cb7e0977a04e510677#code~~">https://sepolia.etherscan.io/address/0x19aea7614a7f2bab5133f4cb7e0977a04e510677#code~~</a></p><p>oh what did we set the parentChainSelector to for eth.. i bet its base sepolia. for crying out loud.</p><p>ok lets deploy child on eth again. third times the charm.</p><p>0: contract Share 0xdaB4AA006F411C5BCA2bF3D7561f3f00C1EB2717 1: contract SharePool 0x97cD63e0b8645A9a40B4a86327b9ed9e8EBFF8A0 2: contract ChildPeer 0xF07A080C018AC30a274CD5c9aDc1433078F9541B 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p>at least the share and pool are auto verified now</p><pre>forge verify-contract \\<br> --chain-id 84532 \\<br> --etherscan-api-key $ETHERSCAN_API_KEY \\<br> --rpc-url $BASE_SEPOLIA_RPC_URL \\<br> 0x9eBa292a8933EA353364C7e5b16498941ac76319 \\<br> ./src/peers/ChildPeer.sol:ChildPeer \\<br> --constructor-args $(cast abi-encode &quot;constructor(address,address,uint64,address,address,address,address,uint64)&quot; &quot;0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59&quot; &quot;0x779877A7B0D9E8603169DdbD7836e478b4624789&quot; 16015286601757825753 &quot;0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238&quot; &quot;0x012bAC54348C0E635dCAc9D5FB99f06F24136C9A&quot; &quot;0xAec1F48e02Cfb822Be958B68C7957156EB3F0b6e&quot; &quot;0xdaB4AA006F411C5BCA2bF3D7561f3f00C1EB2717&quot; 14767482510784806043) \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>and its not verifying. this is the bit i hate.</p><p>the constructor args look right to me</p><p>oh i was verifying the wrong contract address</p><p>here it is: <a href="https://sepolia.etherscan.io/address/0xf07a080c018ac30a274cd5c9adc1433078f9541b#code">https://sepolia.etherscan.io/address/0xf07a080c018ac30a274cd5c9adc1433078f9541b#code</a></p><h3>Ethereum Sepolia Deployments:</h3><p>Share/YieldCoin:</p><p><a href="https://sepolia.etherscan.io/address/0xdaB4AA006F411C5BCA2bF3D7561f3f00C1EB2717#code">https://sepolia.etherscan.io/address/0xdaB4AA006F411C5BCA2bF3D7561f3f00C1EB2717#code</a></p><p>SharePool:</p><p><a href="https://sepolia.etherscan.io/address/0x97cD63e0b8645A9a40B4a86327b9ed9e8EBFF8A0#code">https://sepolia.etherscan.io/address/0x97cD63e0b8645A9a40B4a86327b9ed9e8EBFF8A0#code</a></p><p>ChildPeer:</p><p><a href="https://sepolia.etherscan.io/address/0xf07a080c018ac30a274cd5c9adc1433078f9541b#code">https://sepolia.etherscan.io/address/0xf07a080c018ac30a274cd5c9adc1433078f9541b#code</a></p><p>I sent the Eth Child Peer 20 LINK. dont think i added a function to withdraw link so hopefully it won’t need to be redeployed. i dont think it will, will just have to set allowed peers</p><h3>Deploy child peer on base sepolia</h3><p>forge script script/deploy/DeployChild.s.sol --rpc-url $BASE_SEPOLIA_RPC_URL --account myKeystore --broadcast</p><p>0: contract Share 0xAb96743Df71874074628870C0951Bb54C300522c 1: contract SharePool 0x0fc34866bb8558DF68c1259FB04d62187C5A17d6 2: contract ChildPeer 0xe814080e30647a0E7Ce6f73bAAa66BFBDE748501 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><h3>Base Sepolia Deployments</h3><p>base share/yieldcoin: <a href="https://sepolia.basescan.org/address/0xab96743df71874074628870c0951bb54c300522c">https://sepolia.basescan.org/address/0xab96743df71874074628870c0951bb54c300522c</a></p><p>base sharePool:</p><p><a href="https://sepolia.basescan.org/address/0x0fc34866bb8558df68c1259fb04d62187c5a17d6#code">https://sepolia.basescan.org/address/0x0fc34866bb8558df68c1259fb04d62187c5a17d6#code</a></p><p>base ChildPeer:</p><p><a href="https://sepolia.basescan.org/address/0xe814080e30647a0e7ce6f73baaa66bfbde748501">https://sepolia.basescan.org/address/0xe814080e30647a0e7ce6f73baaa66bfbde748501</a></p><p>sent 20 link to that peer</p><p>will need to set ccipGasLimit on these contracts, as well as allowed peers.</p><h3>Deploy Parent on Avalanche Fuji</h3><p>aww no this is gonna require that lame flattening thing again</p><p>forge script script/deploy/DeployParent.s.sol --rpc-url $AVALANCHE_FUJI_RPC_URL --account myKeystore --broadcast</p><p>and now the keystore password is failing to decrypt.</p><p>………….</p><p>this kind of thing is exactly why im doing this 2 weeks before the submission deadline</p><p>WHY DID IT WORK WITH THE MOCKS ON AVALANCHE</p><p>Error: Failed to decode return value: 0x</p><p>else if (block.chainid == 43113) activeNetworkConfig = getAvalancheFujiConfig();</p><p>was missing this line in the helperConfig. worrying if we deployed on fuji earlier for the mock aave stuff, surely that means the correct usdc address wasnt included then.</p><p>how do we check</p><p>`cast storage</p><p>i think its a waste of time checking, i already know</p><p>0: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 1: contract MockAaveNoYield 0x63fe7cd6a0c966ecE0ef94554c451F7a2714E13c 2: contract MockAToken 0x2324080D042eD9b818cA2e7a575c920002Bd2fAB 3: contract MockPoolAddressesProvider 0x5A7707c69738242CEEb22adEe0E24c8C8aff4d0E</p><p>at least these are verified now</p><p><a href="https://testnet.snowtrace.io/address/0x63fe7cd6a0c966ecE0ef94554c451F7a2714E13c/contract/43113/code">https://testnet.snowtrace.io/address/0x63fe7cd6a0c966ecE0ef94554c451F7a2714E13c/contract/43113/code</a></p><p>back to deploying parent</p><p>0: contract Share 0x53C536023Df836289c44367C3A19e674d407C601 1: contract SharePool 0x3F96E517e33309835430d2cc07c0241Db1219e5C 2: contract ParentCLF 0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p>oh snowtrace does have the api option for verification <a href="https://testnet.snowtrace.io/documentation/recipes/foundry-verification">https://testnet.snowtrace.io/documentation/recipes/foundry-verification</a></p><p><a href="https://testnet.snowtrace.io/documentation#api-plans">https://testnet.snowtrace.io/documentation#api-plans</a> cant figure this out though. they want $200 to verify a contract via api? someone needs to fix something here. either the entire thing needs to be redone or the documentation needs serious work because none of these pages are helping.</p><p>forge verify-contract — chain-id 43113 0x53C536023Df836289c44367C3A19e674d407C601 ./src/token/Share.sol:Share</p><p><a href="https://build.avax.network/docs/dapps/toolchains/foundry#verifying-after-deployment">https://build.avax.network/docs/dapps/toolchains/foundry#verifying-after-deployment</a></p><p>this didnt help either. why cant snowtrace just have an etherscan api key</p><pre>forge verify-contract \\<br> --chain-id 43113 \\<br> --etherscan-api-key $ETHERSCAN_API_KEY \\<br> --rpc-url $AVALANCHE_FUJI_RPC_URL \\<br> 0x53C536023Df836289c44367C3A19e674d407C601 \\<br> ./src/token/Share.sol:Share \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>fine [obscenities] i’ll flatten it</p><p>ava share: <a href="https://testnet.snowtrace.io/address/0x53C536023Df836289c44367C3A19e674d407C601/contract/43113/code">https://testnet.snowtrace.io/address/0x53C536023Df836289c44367C3A19e674d407C601/contract/43113/code</a></p><p>now i need to flatten the share pool and parent clf</p><p>“Our API service is designed to provide developers with seamless access to blockchain data across multiple networks. Our free tier allows you to use the API without requiring an API key”</p><p><a href="https://build.avax.network/docs/dapps/toolchains/foundry#compile-deploy-and-verify-with-forge">https://build.avax.network/docs/dapps/toolchains/foundry#compile-deploy-and-verify-with-forge</a></p><p>this bit talking about snowtrace api key is out of date</p><p>yep</p><p>ok let’s flatten the share pool</p><p>ok fair enough not having to do constructor args for these flattened files with snowtrace is very convenient</p><p>ava sharePool: <a href="https://testnet.snowtrace.io/address/0x3F96E517e33309835430d2cc07c0241Db1219e5C/contract/43113/readContract?chainid=43113">https://testnet.snowtrace.io/address/0x3F96E517e33309835430d2cc07c0241Db1219e5C/contract/43113/readContract?chainid=43113</a></p><p>Parent:</p><p><a href="https://testnet.snowtrace.io/address/0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e/contract/43113/readContract?chainid=43113">https://testnet.snowtrace.io/address/0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e/contract/43113/readContract?chainid=43113</a></p><h3>Deployments (old)</h3><h3>Avalanche Fuji</h3><h4>ParentPeer (Avalanche)</h4><p><a href="https://testnet.snowtrace.io/address/0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e/contract/43113/readContract?chainid=43113">https://testnet.snowtrace.io/address/0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e/contract/43113/readContract?chainid=43113</a></p><p>0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e</p><h4>Share/YieldCoin (Avalanche)</h4><p><a href="https://testnet.snowtrace.io/address/0x53C536023Df836289c44367C3A19e674d407C601/contract/43113/code">https://testnet.snowtrace.io/address/0x53C536023Df836289c44367C3A19e674d407C601/contract/43113/code</a></p><p><a href="https://testnet.snowtrace.io/address/0x53C536023Df836289c44367C3A19e674d407C601/contract/43113/code">0x53C536023Df836289c44367C3A19e674d407C601</a></p><h4>SharePool (Avalanche)</h4><p><a href="https://testnet.snowtrace.io/address/0x3F96E517e33309835430d2cc07c0241Db1219e5C/contract/43113/readContract?chainid=43113">https://testnet.snowtrace.io/address/0x3F96E517e33309835430d2cc07c0241Db1219e5C/contract/43113/readContract?chainid=43113</a></p><h4>Mock Compound (Avalanche)</h4><p><a href="https://testnet.snowtrace.io/address/0x43a5Ddb9561762D835B6c0f15Cb8a7ed02F6D958/contract/43113/code">https://testnet.snowtrace.io/address/0x43a5Ddb9561762D835B6c0f15Cb8a7ed02F6D958/contract/43113/code</a></p><h4>Mock Aave Pool</h4><p><a href="https://testnet.snowtrace.io/address/0x63fe7cd6a0c966ecE0ef94554c451F7a2714E13c/contract/43113/code">https://testnet.snowtrace.io/address/0x63fe7cd6a0c966ecE0ef94554c451F7a2714E13c/contract/43113/code</a></p><h4>Mock Aave Addresses Provider</h4><p><a href="https://testnet.snowtrace.io/address/0x5A7707c69738242CEEb22adEe0E24c8C8aff4d0E/contract/43113/code">https://testnet.snowtrace.io/address/0x5A7707c69738242CEEb22adEe0E24c8C8aff4d0E/contract/43113/code</a></p><h4>Mock aToken</h4><p><a href="https://testnet.snowtrace.io/address/0x2324080D042eD9b818cA2e7a575c920002Bd2fAB/contract/43113/code?chainid=43113">https://testnet.snowtrace.io/address/0x2324080D042eD9b818cA2e7a575c920002Bd2fAB/contract/43113/code?chainid=43113</a></p><h3>Ethereum Sepolia</h3><h4>ChildPeer (Eth)</h4><p><a href="https://sepolia.etherscan.io/address/0xf07a080c018ac30a274cd5c9adc1433078f9541b#code">https://sepolia.etherscan.io/address/0xf07a080c018ac30a274cd5c9adc1433078f9541b#code</a></p><h4>Share/YieldCoin (Eth)</h4><p><a href="https://sepolia.etherscan.io/address/0xdaB4AA006F411C5BCA2bF3D7561f3f00C1EB2717#code">https://sepolia.etherscan.io/address/0xdaB4AA006F411C5BCA2bF3D7561f3f00C1EB2717#code</a></p><h4>SharePool (Eth)</h4><p><a href="https://sepolia.etherscan.io/address/0x97cD63e0b8645A9a40B4a86327b9ed9e8EBFF8A0#code">https://sepolia.etherscan.io/address/0x97cD63e0b8645A9a40B4a86327b9ed9e8EBFF8A0#code</a></p><h3>Base Sepolia</h3><h4>ChildPeer (Base)</h4><p><a href="https://sepolia.basescan.org/address/0xe814080e30647a0e7ce6f73baaa66bfbde748501">https://sepolia.basescan.org/address/0xe814080e30647a0e7ce6f73baaa66bfbde748501</a></p><h4>Share/YieldCoin (Base)</h4><p><a href="https://sepolia.basescan.org/address/0xab96743df71874074628870c0951bb54c300522c">https://sepolia.basescan.org/address/0xab96743df71874074628870c0951bb54c300522c</a></p><h4>SharePool (Base)</h4><p><a href="https://sepolia.basescan.org/address/0x0fc34866bb8558df68c1259fb04d62187c5a17d6#code">https://sepolia.basescan.org/address/0x0fc34866bb8558df68c1259fb04d62187c5a17d6#code</a></p><h4>Mock Aave Pool</h4><p><a href="https://sepolia.basescan.org/address/0x3c1a3fd4f5eb59c2746ba2bafb10c4ef8b745787#code">https://sepolia.basescan.org/address/0x3c1a3fd4f5eb59c2746ba2bafb10c4ef8b745787#code</a></p><h4>Mock Aave Addresses Provider</h4><p><a href="https://sepolia.basescan.org/address/0xc88543241be4a46ca5afc2d44100a0cc767d6b78#code">https://sepolia.basescan.org/address/0xc88543241be4a46ca5afc2d44100a0cc767d6b78#code</a></p><h4>Mock aToken</h4><p><a href="https://sepolia.basescan.org/address/0x6fdbc908533d16983e3c0d91027ea78fd2a84e9b#code">https://sepolia.basescan.org/address/0x6fdbc908533d16983e3c0d91027ea78fd2a84e9b#code</a></p><p>All of these Peers have LINK tokens. now we need a script to set crosschain vars and probably remote token pool stuff</p><p>only the parent peer needs its own selector to be allowed i think because of the clf callback check</p><p>attack vector to think about:</p><ol><li>ccip rebalance tx fails and funds are stuck on chain a</li><li>parent.strategy.selector points to chain b</li><li>next rebalance call</li></ol><p>forge script script/interactions/SetCrosschain.s.sol --account myKeystore --broadcast --rpc-url $ETH_SEPOLIA_RPC_URL</p><p>the script works nicely, but its not able to be exactly tested because i cant prank my deployer before running it in unit test on forked fuji because prank and broadcast are incompatible</p><p>i need to update the chain selectors in the src.js/src.min.js scripts. then i need to deposit usdc into the parent and register chainlink automation</p><p>cast send 0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e --rpc-url $AVALANCHE_FUJI_RPC_URL &quot;deposit(uint256)” 20000000 --account myKeystore</p><p>cast send 0x5425890298aed601595a70AB815c96711a31Bc65 --rpc-url $AVALANCHE_FUJI_RPC_URL &quot;approve(address,uint256)&quot; 0xf5Ce3A71d39b1e493879b86B3e1e0d6906916a4e 20000000 --account myKeystore</p><p>now i cant even deposit into my parent contract</p><p>snowtrace frontend gives some wagmi error</p><p>and cast send returns</p><pre>Error: Internal transport error: No such file or directory (os error 2) with /Users/anon/repos/work/contractlevel/hackathons-events/yield/0xb6b55f25</pre><p>i tried with the selector 0xb6b55f25 and it still didnt work</p><p>i should probably go to bed</p><p>i think i need to set the peer on the mock aave and compound contracts</p><p>i set the peers manually on snowtrace and the peer for mock aave on base.</p><p>i just tried to deposit and it failed <a href="https://testnet.snowtrace.io/tx/0xff6ce5829cfbb6df371dc6a630b079ad635d4a7e618524edeebbc99cec88d427?chainid=43113">https://testnet.snowtrace.io/tx/0xff6ce5829cfbb6df371dc6a630b079ad635d4a7e618524edeebbc99cec88d427?chainid=43113</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L8eaCZPIK7Bu87VKV8-mpw.png" /></figure><p>reverting here so the implication is MockAaveNoYield::i_usdc didnt get set correctly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yVlaM4aSnpHrVqnmawA68w.png" /></figure><p>0xd9003177dc465aaa89e20678675dca7fa5f5cad5</p><p>this address isnt anything. is this what the mock aave pool thinks is usdc??? where did it come from?</p><p>this is from the mock pool deploy script and what i <em>suspect</em> is the cause of th issue</p><pre>HelperConfig config = new HelperConfig();<br> HelperConfig.NetworkConfig memory networkConfig = config.getActiveNetworkConfig();<br> vm.startBroadcast();<br> MockAaveNoYield mockAaveNoYield = new MockAaveNoYield(networkConfig.tokens.usdc);</pre><p>i dont know for sure but i am assuming that because the networkConfig is getting cached BEFORE the broadcast, then the active network config is the local anvil settings, whereas we want the avalanche testnet</p><p>lets see what we get</p><pre>vm.startBroadcast();<br>        HelperConfig.NetworkConfig memory networkConfig = config.getActiveNetworkConfig();<br>        console2.log(&quot;networkConfig.tokens.usdc&quot;, networkConfig.tokens.usdc);<br>        MockAaveNoYield mockAaveNoYield = new MockAaveNoYield(networkConfig.tokens.usdc);</pre><p>0: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 1: contract MockAaveNoYield 0xF07A080C018AC30a274CD5c9aDc1433078F9541B 2: contract MockAToken 0x7B90F28a6eF4597317B02b2805b89bDc07b7AcFA 3: contract MockPoolAddressesProvider 0xc314344EA3676CD43EAc7c9B02B00e6cfE1Af774</p><p>== Logs == networkConfig.tokens.usdc 0x5425890298aed601595a70AB815c96711a31Bc65</p><p>good this is more like it.</p><p>i guess we better</p><ul><li>redeploy the parent clf on avalanche, add that as a consumer to clf,</li><li>set parent clf peer on new mock aave and old mock compound</li><li>redeploy child peer on base and new mock aave</li><li>set child peer on mock aave</li><li>set allowed chains and peers</li></ul><p>we probably dont need to redeploy the shares and sharepools but whatever i cant be bothered rewriting the script</p><h3>New Deployments</h3><p><strong>Avalanche Fuji:</strong></p><p>0: contract Share 0xc84fAcbCEaaB9dbc27C14fB553b2c25C1b794497 1: contract SharePool 0xAFbfb8e05c865213FdF795eA520C6D2241537Cb1 2: contract ParentCLF 0x7406dBE173967a005585129Df7728caB243BF235 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 1: contract MockAaveNoYield 0xF07A080C018AC30a274CD5c9aDc1433078F9541B 2: contract MockAToken 0x7B90F28a6eF4597317B02b2805b89bDc07b7AcFA 3: contract MockPoolAddressesProvider 0xc314344EA3676CD43EAc7c9B02B00e6cfE1Af774</p><p>Mock comet: 0x43a5Ddb9561762D835B6c0f15Cb8a7ed02F6D958</p><p><strong>Base sepolia:</strong></p><p>1: contract MockAaveNoYield 0x2Ed4E8435eFf62Eb48E613159a6a5Fe86b19fa16 2: contract MockAToken 0x2891C37D5104446d10dc29eA06c25C6f0cA233Ec 3: contract MockPoolAddressesProvider 0x9bf12E915461A48bc61ddca5f295A0E20BBBa5D7</p><p>0: contract Share 0x41a80C30B1DdC0752871393e68D4773E16Ae2fD2 1: contract SharePool 0xAF1B97Bb3f6809813a50cB99c892872b3F0B6EFb 2: contract ChildPeer 0x9eBa292a8933EA353364C7e5b16498941ac76319</p><pre>forge verify-contract \\<br> --chain-id 84532 \\<br> --etherscan-api-key $ETHERSCAN_API_KEY \\<br> --rpc-url $BASE_SEPOLIA_RPC_URL \\<br> 0x9eBa292a8933EA353364C7e5b16498941ac76319 \\<br> ./src/peers/ChildPeer.sol:ChildPeer \\<br> --constructor-args $(cast abi-encode &quot;constructor(address,address,uint64,address,address,address,address,uint64)&quot; &quot;0xD3b06cEbF099CE7DA4AcCf578aaebFDBd6e88a93&quot; &quot;0xE4aB69C077896252FAFBD49EFD26B5D171A32410&quot; 10344971235874465080 &quot;0x036CbD53842c5426634e7929541eC2318f3dCF7e&quot; &quot;0x9bf12E915461A48bc61ddca5f295A0E20BBBa5D7&quot; &quot;0x571621Ce60Cebb0c1D442B5afb38B1663C6Bf017&quot; &quot;0xAb96743Df71874074628870C0951Bb54C300522c&quot; 14767482510784806043) \\<br> --compiler-version 0.8.26 \\<br> --optimizer-runs 1000000</pre><p>why is this not working……</p><p>oh wrong share address</p><h3>Successful Deposit Transaction</h3><p>I have successfully deposited usdc into the CLY system which has deposited it into the current strategy (on parent chain) and minted YieldCoin in return.</p><p><a href="https://testnet.snowtrace.io/tx/0x9ac744d0e47176aef4df60b66e6e50901b6bc5bb97e75e722971185e0e13eb82?chainid=43113">https://testnet.snowtrace.io/tx/0x9ac744d0e47176aef4df60b66e6e50901b6bc5bb97e75e722971185e0e13eb82?chainid=43113</a></p><p>Very cool. The next step is adjusting the src.js/src.min.js to use testnet chainselectors and register time based automation, and then we will be able to see if the automated rebalancing actually works</p><p>We need: Avalanche, Ethereum, and Base testnet chain selectors</p><h3>YieldCoin branding</h3><p>whatever the highest yield is for stablecoins across chains is what you can earn in one click with contract level yield</p><p>problem statement: i want to hold my stablecoin and earn the highest possible yield for it without having to monitor opportunities, then manually withdraw, bridge and deposit. YieldCoin abstracts ALL of that away. Deposit your stablecoin into the CLY system from your chain of choice to earn the highest yield from the safest, most reliable services across the web3 ecosystem.</p><p>Stablecoin depositors receive a share token in return for their deposits, representing their share of the total value (deposits + yield) in the system. Depositing a stablecoin can also be considered buying YieldCoin. YieldCoin is the share received for depositing into the system, with the basic idea being that a holder will be able to sell for a higher USD value than they bought it. This is because the stablecoin deposits will not go down in value, and reliable yield will be generated. Hence the name YieldCoin.</p><p>key invariant: a user must be able to withdraw the usdc amount they deposited — fees. this is definitely broken by the bug we already know</p><p>YieldCoin follows the ERC677 and CCT standards for maximum efficiency and interoperability. The YieldCoin CCIP pool is permissionless, allowing holders to move freely across chains.</p><p>The more fees CLY generates, ie the more YieldCoin is bought, the more frequent the checks for the highest APY can become, as Chainlink fees are covered.</p><p>“one click highest yield”</p><p>back to the project</p><p>i updated the script to use testnet selectors, i need to set crosschain vars again for every chain</p><p>ok did that</p><p>i think we’re ready for cla to see if rebalancing works</p><p>the automation call worked and the functions callback worked (although i dont see any CLFRequestSent events from my ParentPeer on snowtrace)</p><p>HOWEVER the functions computation failed, im pretty sure for the reason that occurred to me earlier which is that i didnt change the secrets in the parentPeer constant and the params used when encrypting that stuff includes the router address, so of course its base sepolia. maybe i’ll just make the parent base sepolia</p><pre>const secretsManager = new SecretsManager({<br>      signer: wallet,<br>      functionsRouterAddress: routerAddress,<br>      donId: donId,<br>      // gatewayUrls: [&#39;&lt;https://01.functions-gateway.testnet.chain.link/&gt;&#39;],<br>    });</pre><p>the question right now is, what would be quicker?</p><p>deploying parentpeer on base and childpeer on avalanche, setting crosschain vars, and funding child and parent with link again</p><p>or</p><p>encrypting secrets with avalanche params, replacing constants in parentPeer, redeploying parentpeer on avalanche, setting crosschain vars, and funding parentpeer with link again</p><p>in the time spent thinking about this, one or the other couldve been done. lets just try encrypting secrets again with avalanche params and if too many issues come up from that, then go with the first</p><h3>Base Secret:</h3><pre>bytes internal constant ENCRYPTED_SECRET =<br>        hex&quot;1b4e2d1a565a496c987dc6d8303c52370378b6dcecfd8c1a11b7141822be2b3cb96daba5632633858fe0f07fd5ac85b5293101588003a9f260b0c3d1134765de7b9b16bc1d9087affbf117c4b40a259bc618892ade4707190950fd320e239e36e1f16d577e8c9ed52bad67544530dbca3d846dc4b1f3f8eb6c7a6346172762f449be0d5bcfac35dde9a342db2e009d19c87facf05164b6a3ea257c5a687190b38ec8fd117bfc6a27d7da08abb980afb3aca40b71adbf1cf823b6a3b7bda583e3dc76f1e828bb991398c0b0ee96a2b6c5414c86f62e317286480850e80e8d6ab32718640ef44135f7a875b29e757566a823&quot;;</pre><h3>Avalanche Secret:</h3><pre>bytes internal constant ENCRYPTED_SECRET = <br>		hex&quot;c37b08028a80330e0fb5c784f636424f03660be7ac1007bbdefe1a9456a3008634433ceccdb128bbd58b9c95125b3ad42751e7ba7b01f435833b30a07402ad44832b0110ad06a7c9f764cdb939292e1df97ee18ba46e93b3814ff7e205926ae072100aff074289c22d9eb342e8832790431accc8dd4e9dc868d21d944966f6c67a7574fa09c8d9c67119b0bec146f87f986d7ed9550cedef7e3c75b7feaee0722b4c82295f0c02eab7ecec08c1d22e27906179bc9dd2d11336b42793e13f24c43b2a9b993967551ff7fe619f0a31212538f973efd33d9955ac713b16717a899a17a58e8ee21160814b6464ebff934c35cdd5bd31b50a6074c08ecfb0330e435c2d&quot;;</pre><p>0: contract Share 0xCa2B9703bE8E6188850c16e72d28269C11783486 1: contract SharePool 0x08aa5a8e751e4b04333d3defe3825c63a7bfF642 2: contract ParentCLF 0x7e60c39e5ecE77BC7a011A297F43775FcAbc8A88 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p>i should try withdrawing the deposited usdc from the old deployment</p><h3>Successful withdraw transaction on parent strategy</h3><p><a href="https://testnet.snowtrace.io/tx/0x42f3a838c9fe316a932eaa4d245d8394a98b0b9ca84049a527b4966dddca7fc1?chainid=43113">https://testnet.snowtrace.io/tx/0x42f3a838c9fe316a932eaa4d245d8394a98b0b9ca84049a527b4966dddca7fc1?chainid=43113</a></p><p>to be fair, besides the blockexplorer verification, i do like how fast avalanche’s testnet is</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P9Dm4dahYRDD48vZKhksBg.png" /></figure><p>this again, in the interest of time im pretty close to saying disregard avalanche and changing the parent back to base sepolia, since we already know the secret works there</p><h3>Deployment Steps:</h3><ol><li>deploy child script — eth</li><li>replace config constant eth addresses</li><li>fund with link</li><li>deploy child script — avax</li><li>replace config constant ava addresses</li><li>setPeer in aave mock and compound mock</li><li>fund with link</li><li>deploy parent script — base</li><li>replace config constant base addresses</li><li>setPeer in aave mock bae</li><li>fund with link</li><li>run set crosschain vars script for each chain</li><li>add functions consumer</li><li>approve peer usdc</li><li>deposit usdc</li><li>register automation</li></ol><p>i forgot to change the parent chain selector. that means we need to deploy the eth contracts too</p><h3>Eth Child Deployment</h3><p>0: contract Share 0xFF86A7bf3faD9c09AD0BAD7C81FFF95704CA3fF8 1: contract SharePool 0x878782d1AA5E5C2F05acB3f78A63641eBd6FD3B9 2: contract ChildPeer 0xEF394A35140cfbcD0AdCD83334524688b327fD5d</p><h3>Avalanche Child Deployment</h3><p>0: contract Share 0x7396A6424479b5EfA37601ac90959e8e684750Cf 1: contract SharePool 0x69ddcF861d49C4d157332aa5854D69E11e0C2d8f 2: contract ChildPeer 0x0b87C25427da8bf1408642D5BC7DaA8246bf60c3</p><h3>Base Parent Deployment</h3><p>0: contract Share 0x9f02a7bD79Be3BeaFd1847BD6B308342D6201512 1: contract SharePool 0xC6c48022e8091A091fd0eFcce69234aF45dc9Fb4 2: contract ParentCLF 0x2eCAa0b9724C369EF913af8459561643eC78FAd8</p><h3>New transactions</h3><p>deposit into base parent: <a href="https://sepolia.basescan.org/tx/0x532ffe95018565ddf9ad058bdf1f2a9cea2a4e576f123451ccea33bc6686d2e9">https://sepolia.basescan.org/tx/0x532ffe95018565ddf9ad058bdf1f2a9cea2a4e576f123451ccea33bc6686d2e9</a></p><p>automation call: <a href="https://sepolia.basescan.org/tx/0xe0e041dcd7fbe38025eb967c46d17bbbd1bdf43bce7dc017abd1926a32672584">https://sepolia.basescan.org/tx/0xe0e041dcd7fbe38025eb967c46d17bbbd1bdf43bce7dc017abd1926a32672584</a></p><p>oops, forgot to set the allowed chain selectors, but the functions callback and computation was a success.</p><p>automation call:</p><p><a href="https://sepolia.basescan.org/tx/0xcacdfd47362a4fd038221d5c70880542e764d54b3e01c6fd367520071bd5502a">https://sepolia.basescan.org/tx/0xcacdfd47362a4fd038221d5c70880542e764d54b3e01c6fd367520071bd5502a</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jjRUcsKycA_Oi3fs3KW1Ug.png" /></figure><p>my first reaction to this is that the ccip tx was more gas than the clf callback allowed and that i’d have to implement custom logic or log trigger automation to execute the ccip tx based on the clf callback.</p><p>but tenderly is showing a revert happening in the usdc token pool</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gk50Z2XdZ3VmBI-SbCh_ug.png" /></figure><p>tx from clf ui: <a href="https://sepolia.basescan.org/tx/0xd3b5f26f847d3b1460c6d316b56dae8399766c3b9db534bc41003e025a78138b#eventlog">https://sepolia.basescan.org/tx/0xd3b5f26f847d3b1460c6d316b56dae8399766c3b9db534bc41003e025a78138b#eventlog</a></p><pre>(<br>FunctionsRouter<br> =&gt; <br>ParentCLF<br>)<br>.<br>handleOracleFulfillment<br>(requestId = 0x0099…2a70<br>0x009984bba5f90090cdb07a86be07364df267ecb8f7bfaf73a12fe69a0bed2a70<br>, response = 0x0000…0000<br>0x000000000000000000000000000000000000000000000000de41ba4fc9d91ad90000000000000000000000000000000000000000000000000000000000000000<br>, err = 0x)<br>=&gt;<br>()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZCtkLgfxGYWCfJEyFtNJmg.png" /></figure><p>why is the destination chain selector 16015286601757825000?</p><p>eth sepolia is 16015286601757825753, which is set to allowed in our parent, and the clf callback wouldve returned if the decoded response didnt match that</p><p>i made a post in the chainlink discord because this makes no sense</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*epa8xvRbq1Dkh71-i-6Hyw.png" /></figure><p><a href="https://sepolia.etherscan.io/address/0x5931822f394babc2aacf4588e98fc77a9f5aa8c9#code">https://sepolia.etherscan.io/address/0x5931822f394babc2aacf4588e98fc77a9f5aa8c9#code</a></p><p>this contract is returned by Router=&gt;EVM2EVMOnRamp).getPoolBySourceToken(0 = 16015286601757825000, sourceTokn =0x036cbd53842c5426634e7929541ec2318f3dcf7e)=(0x5931822f394babc2aacf4588e98fc77a9f5aa8c9)</p><p>that’s the weird selector with 0’s, and the address on eth mainnet is <strong>HybridLockReleaseUSDCTokenPool, but the sep one is unverified</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2auecRDQyD8O-4LXptAgyg.png" /></figure><p>“lockOrBurnIn = null”</p><p>hmmm why is that</p><p>the line above that is lockOrBurnIn =</p><pre>{<br>&quot;receiver&quot;:&quot;0x000000000000000000000000ef394a35140cfbcd0adcd83334524688b327fd5d&quot;,<br>&quot;remoteChainSelector&quot;:16015286601757825000,<br>&quot;originalSender&quot;:&quot;0x2ecaa0b9724c369ef913af8459561643ec78fad8&quot;,<br>&quot;amount&quot;:&quot;10000000&quot;,<br>&quot;localToken&quot;:&quot;0x036cbd53842c5426634e7929541ec2318f3dcf7e&quot;<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*91YWDQ_Kg0QSXLY2t0IyIQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JRRrIh59tWo5oCeaGMQhWQ.png" /></figure><p>This sounds like my initial reaction was correct. I’ll try this:</p><ol><li>comment out ccipSend and see if the rest of the clf callback is a success</li><li>ccipSend usdc directly(?)</li><li>implement log trigger or whatever automation and see if that fixes it. might just do this last one first</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FK6ejzyvm_gfuQQxVJMB_g.png" /></figure><p>im already using the max clf callback gas limit so i doubt the 2nd reason is the one.</p><h3>Steps:</h3><ol><li>deploy parentCLA on base</li><li>setPeer on mock aave</li><li>copy constants helper config</li><li>run set crosschain vars for all chains</li><li>approve, deposit usdc</li><li>fund with link</li><li>register time based auto, set upkeep address</li><li>add clf consumer</li><li>register log trigger auto, set forwarder</li></ol><pre>== Return ==<br>0: contract Share 0x626805176fF3CBB5A9CBA8d8529E972a954516AA<br>1: contract SharePool 0x216AB6622F264092aefEB759a6F0EF9d4A70C670<br>2: contract ParentCLA 0x9033781dE2fb23Bc66e8E2701cCCA84644BF7A99<br>3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3<br>4: uint64 333<br>Error: `Unknown1` is above the contract size limit (25375 &gt; 24576).</pre><p>this isnt good</p><p>i’d need to split up the parent contract if i want to implement more than what i’ve got with ParentCLF…….. arghgheheg lets try deploying parentCLF and doing the CLF and ccip separately</p><p>DEBUG TEST CONTRACTS:</p><p>0: contract Share 0x74eC127bAc1462FF639A12e3a7CB4601d5aB97DF 1: contract SharePool 0xE0C2598a1aC1D6ec120607F1e09caE229aaBD8c8 2: contract ParentCLF 0x2642E586c52E35A7C44995ea74a8A025651ba6BD 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p><a href="https://sepolia.basescan.org/tx/0xc0109b56cd996346575c9321af95e3eb4cc38bb28154b53e5adad4676d1f8c8b">https://sepolia.basescan.org/tx/0xc0109b56cd996346575c9321af95e3eb4cc38bb28154b53e5adad4676d1f8c8b</a> this tx confirms the CLF callback upto the CCIP tx works fine.</p><p><a href="https://sepolia.basescan.org/tx/0x502ff28d3960ca399d610e52f4cb9e98c70f163af681bc25e999ae99ede188b2">https://sepolia.basescan.org/tx/0x502ff28d3960ca399d610e52f4cb9e98c70f163af681bc25e999ae99ede188b2</a> this tx confirms the ccip send is working too. <a href="https://ccip.chain.link/#/side-drawer/msg/0xce2b94dc57dd5ea358a7d663ea018d609fc7bdbb2c50b7e8acdb017448593e34">https://ccip.chain.link/#/side-drawer/msg/0xce2b94dc57dd5ea358a7d663ea018d609fc7bdbb2c50b7e8acdb017448593e34</a></p><p>soooo yeah looks like the parent needs to be split up into multiple contracts to handle:</p><ol><li>CLF</li><li>CLA time based</li><li>CLA log trigger</li></ol><p>these 3 should be 1 contract and then the other parent stuff should be in another</p><p>probably</p><p>i need to think about this</p><p>maybe the log trigger auto can be in a ParentHelper contract, and that calls the ParentCLF</p><p>alright ParentHelper is called ParentRebalancer and it implements the log trigger automation to call ccipSend for ParentCLF based on update strategy logs</p><p>base sepolia:</p><p>0: contract ParentRebalancer 0x14E698b4E5d2883B1Afb9383c0c864f7cf6a7a16</p><p>[obscenities]</p><p>LOG BASED TRIGGER AUTOMATION IS NOT AVAILABLE ON BASE SEPOLIA</p><p>WHY DO THEY DO THAT</p><p>The last few days have been a difficult experience</p><p>so stressful.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D7b5wTarbjqXbRJsVoUyZg.png" /></figure><p>i am so over this hackathon. what a stressful experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I2Bs4s3ro0FgEaS2q4XLTg.png" /></figure><p>this is [obscenities] too. everytime i try and look at an address on base sepolia explorer i get this.</p><p>manual execution on eth sepolia failed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Orztr8YIpkfWMTGS9zlkQ.png" /></figure><p>im so stressed out now. let’s list the current problems</p><ol><li>original CLA → CLF → CCIP rebalance failed (probably cus of too much gas for CLF)</li><li>log trigger automation unavailable on base sepolia</li><li>ccip tx failing on destination</li></ol><p>time constraints i hate time constraints let me work indefinitely</p><p>where do we start with these problems? well i applied for base sepolia log trigger automation. dont know why thats needed for testnet.</p><p><a href="https://docs.chain.link/ccip/tutorials/evm/programmable-token-transfers-defensive">https://docs.chain.link/ccip/tutorials/evm/programmable-token-transfers-defensive</a> should implement everything missed from this later. but i have bigger problems right now.</p><h3>ccip tx failing on destination</h3><p>WHY</p><p><a href="https://dashboard.tenderly.co/tx/0xd23373305560114c0cfc1098719e37a373a13ff486f324f98d7768aacdf95f42?trace=0.1.9">https://dashboard.tenderly.co/tx/0xd23373305560114c0cfc1098719e37a373a13ff486f324f98d7768aacdf95f42?trace=0.1.9</a> tenderly trace</p><p><a href="https://docs.chain.link/ccip/concepts/manual-execution#manual-execution">https://docs.chain.link/ccip/concepts/manual-execution#manual-execution</a></p><p>manual execution is failing with 3_000_000 gas limit, so im assuming theres an issue in the logic of the receiver implementation</p><p>i think the next step is to focus on the ccipReceive logic. i can keep using the last parent peer i deployed on base to test <a href="https://sepolia.basescan.org/address/0x2642e586c52e35a7c44995ea74a8a025651ba6bd">https://sepolia.basescan.org/address/0x2642e586c52e35a7c44995ea74a8a025651ba6bd</a></p><p>to test the usdc transfer works, im just going to set my eoa to the allowed peer, which should ignore the data and just send the token</p><p><a href="https://ccip.chain.link/#/side-drawer/msg/0xc0c35c5125d4805980588e3eeac911165ba22b72d0643461560c7e98aa9953d8">https://ccip.chain.link/#/side-drawer/msg/0xc0c35c5125d4805980588e3eeac911165ba22b72d0643461560c7e98aa9953d8</a></p><p>this is the tx sending usdc to eoa from parent</p><p>ok lets just read through the defensive thing and see if handling ccipReceive reverts is going to help</p><p>i added this stuff in YieldPeer:</p><pre>function _ccipReceive(Client.Any2EVMMessage memory message)<br>        internal<br>        override<br>        onlyAllowed(message.sourceChainSelector, abi.decode(message.sender, (address)))<br>    {<br>        // (CcipTxType txType, bytes memory data) = abi.decode(message.data, (CcipTxType, bytes));<br>        // emit CCIPMessageReceived(message.messageId, txType, message.sourceChainSelector);<br>        // _handleCCIPMessage(txType, message.destTokenAmounts, data);<br>        try this.processMessage(message) {<br>            // Intentionally empty in this example; no action needed if processMessage succeeds<br>        } catch (bytes memory err) {<br>            emit MessageFailed(message.messageId, err);<br>            return;<br>        }<br>    }<br>    event MessageFailed(bytes32 indexed messageId, bytes err);<br>    error OnlySelf();<br>    function processMessage(Client.Any2EVMMessage calldata message) external {<br>        if (msg.sender != address(this)) revert OnlySelf();<br>        (CcipTxType txType, bytes memory data) = abi.decode(message.data, (CcipTxType, bytes));<br>        emit CCIPMessageReceived(message.messageId, txType, message.sourceChainSelector);<br>        _handleCCIPMessage(txType, message.destTokenAmounts, data);<br>    }</pre><p>going to deploy this on eth and try sending to it.</p><p><a href="https://ccip.chain.link/#/side-drawer/msg/0x3d7e2b77b5f7662ec86290dfc6bb6752d91ae587f70558310d2071a7c4a083b8">https://ccip.chain.link/#/side-drawer/msg/0x3d7e2b77b5f7662ec86290dfc6bb6752d91ae587f70558310d2071a7c4a083b8</a></p><p>this is the tx we see if that try catch in ccipReceive helps</p><p>….waiting for 30 mins………………</p><p>B O R I N G</p><p>i guess i can write some of the README or update natspec whilst waiting</p><p>ok THAT was a successful tx, lets see what happened on the receiver side. yes it emitted message failed <a href="https://sepolia.etherscan.io/address/0x02997361644e2dba90f0a2245e87577a0fc0bb2f#events">https://sepolia.etherscan.io/address/0x02997361644e2dba90f0a2245e87577a0fc0bb2f#events</a></p><p>well this is good because at least we know where we are</p><p>The ccipReceive logic is failing</p><p>how does this get debugged</p><p>this is old eth child without try catch 0xEF394A35140cfbcD0AdCD83334524688b327fD5d</p><p>lets run that through tenderly</p><p>tenderly ccipreceive param:</p><pre>{<br>  &quot;messageId&quot;: &quot;0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef&quot;,<br>  &quot;sourceChainSelector&quot;: &quot;10344971235874465080&quot;,<br>  &quot;sender&quot;: &quot;0x0000000000000000000000002642e586c52e35a7c44995ea74a8a025651ba6bd&quot;,<br>  &quot;data&quot;: &quot;0x000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000de41ba4fc9d91ad90000000000000000000000000000000000000000000000000000000000000000&quot;,<br>  &quot;destTokenAmounts&quot;: [<br>    {<br>      &quot;token&quot;: &quot;0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238&quot;,<br>      &quot;amount&quot;: &quot;1000000&quot;<br>    }<br>  ]<br>}</pre><p>think i accidentally used the try catch contract instead of the old one</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_ENyAG6Gi4Fs9oa0AHUT6Q.png" /></figure><p>but the tenderly trace revealed the error happening at the decoding bit</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-zTgaLhhGlurgIP7GvQIkQ.png" /></figure><p>are we not encoding the txType or something?</p><pre>function _buildCCIPMessage(<br>        address receiver,<br>        IYieldPeer.CcipTxType txType,<br>        bytes memory data,<br>        Client.EVMTokenAmount[] memory tokenAmounts,<br>        uint256 gasLimit,<br>        address link<br>    ) internal pure returns (Client.EVM2AnyMessage memory evm2AnyMessage) {<br>        evm2AnyMessage = Client.EVM2AnyMessage({<br>            receiver: abi.encode(receiver),<br>            data: abi.encode(txType, data),<br>            tokenAmounts: tokenAmounts,<br>            extraArgs: Client._argsToBytes(Client.GenericExtraArgsV2({gasLimit: gasLimit, allowOutOfOrderExecution: true})),<br>            feeToken: link<br>        });<br>    }</pre><p>the data should definitely decode to (txType, bytes). our unit tests wouldve failed if this wasnt happening….</p><p>maybe this?</p><pre>// (CcipTxType txType, bytes memory data) = abi.decode(message.data, (CcipTxType, bytes));<br>        (uint8 ccipTxTypeEnum, bytes memory data) = abi.decode(message.data, (uint8, bytes));<br>        CcipTxType txType = CcipTxType(ccipTxTypeEnum);<br>        emit CCIPMessageReceived(message.messageId, txType, message.sourceChainSelector);</pre><p>wait no that cant be right, i passed the simulated tx the wrong data. i only encoded the strategy, not the tx type too. lets try that again.</p><p>it’s reverting in the same place with the correct data</p><pre>function test_testTest() public {<br>        IYieldPeer.CcipTxType txType = IYieldPeer.CcipTxType.RebalanceNewStrategy;<br>        IYieldPeer.Strategy memory strategy =<br>            IYieldPeer.Strategy({chainSelector: 16015286601757825753, protocol: IYieldPeer.Protocol.Aave});<br>        bytes memory encodedStrategy = abi.encode(txType, strategy);<br>        console2.logBytes(encodedStrategy);<br>    }<br>    <br> Logs:<br>  0x0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000de41ba4fc9d91ad90000000000000000000000000000000000000000000000000000000000000000</pre><p>maybe i should just decode as uint8 and wrap it</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eKOGyG7_w6JUFoQfHvAX2Q.png" /></figure><p>oops, was still not encoding properly in simulation. need to encode txType with the encoded strategy, not the txType with the strategy</p><p>well that tx was a success…… wtf</p><p>so whats the issue then?!?!?!</p><p>that simulation was a success because the contract had no usdc, so it didnt attempt to deposit anything in a strategy.</p><p>running the simulation with the correct data, shows a revert</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mzfvG5hrpabBEpbEn_7E_Q.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CKf4lL8VqGrwhS3yBLKxxw.png" /></figure><p>so theres a problem with the aave stuff. at least im making progress and not just stuck.</p><p>aave thinks the asset is 0x1c7d4b19c6bc7b0d1743fb6116a902737c728 but the address is invalid on sepolia eth <a href="https://sepolia.etherscan.io/address/0x1c7d4b19c6bc7b0d1743fb6116a902737c728">https://sepolia.etherscan.io/address/0x1c7d4b19c6bc7b0d1743fb6116a902737c728</a></p><p>usdc should be</p><p>0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238</p><p>so that address aave is trying is missing 0s</p><p>is something weird happening in the protocol operations lib?</p><p>if i try to supply manually on aave, i get an error</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Qh4xTCnP2JEdZBmC-9QFmA.png" /></figure><p>so the problem is not with my protocol, it is with aave. that’s good. i guess that means if i deploy mock aave contracts on eth and use those, it should work.</p><p>im executing this tx anyway so i can debug in tenderly</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YXdRCjcYhyYNsxSP9DQe4w.png" /></figure><p>even when manually supplying usdc to aave on sepolia from an eoa, it results with the same revert. that’s good because it confirms the problem is not with my system.</p><p>Well this seems to be where we are at:</p><ul><li>need to deploy mocks on eth</li><li>need to get approved for log trigger automation on base (if this doesnt happen, need to get the secrets to work for eth sepolia which is a huge pain)</li></ul><p>im going to deploy mocks on eth</p><p>0: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 1: contract MockAaveNoYield 0x143A881Dd65a9Ab510AD181A96532aFe97cCE8c5 2: contract MockAToken 0x3571301e1C9037D6E9CB349096e9ea8C1F920566 3: contract MockPoolAddressesProvider 0xf7869E9D4227c53AbCD5b4964fC7f502C2FC7A58</p><p>== Logs == networkConfig.tokens.usdc 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238</p><p>i should probably test the compound works on base and eth sepolia</p><p>depositing and withdrawing with compound on eth sep works. ok compound works on base too.</p><p>i think now its time to redeploy the eth child with the new aave mock and attempt ccip send again</p><p>0: contract Share 0x94740bf86f6fAf65020D397968b53eD132f0974f 1: contract SharePool 0x0b87C25427da8bf1408642D5BC7DaA8246bf60c3 2: contract ChildPeer 0x6fDbc908533d16983E3c0d91027eA78fd2A84E9B 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p><a href="https://ccip.chain.link/#/side-drawer/msg/0x06ad7536486a4a29d9b43f18878f44420af08c93c0cdf9de2050211c1edb8cd7">https://ccip.chain.link/#/side-drawer/msg/0x06ad7536486a4a29d9b43f18878f44420af08c93c0cdf9de2050211c1edb8cd7</a></p><p>ok this is the tx where we will see if ccipSend will successfully deposit into a strategy on the other side</p><p>what can i do for the next half hour? i’ll go to the toilet for a start then think about log trigger automation</p><p>I need access to log trigger automation on base sepolia. it makes no sense why they would restrict it for a testnet. it was like this over a year ago. i swear so many issues i’ve had with testing chainlink have remained the same for over a year, well usdc not being compatible with ccip local sim and no log trigger automation for base sepolia anyway.</p><p><a href="https://sepolia.etherscan.io/address/0x6fdbc908533d16983e3c0d91027ea78fd2a84e9b#events">https://sepolia.etherscan.io/address/0x6fdbc908533d16983e3c0d91027ea78fd2a84e9b#events</a></p><p>amazing, the ccipReceive successfully deposited into the aave strategy on eth sepolia from base sepolia</p><p>ok so literally all i need is log trigger automation to trigger ccipSend based on CLF callback.</p><p>as soon as that is working, i can start the frontend (urgh)</p><p>Chromion hackathon: YieldCoin is powered by the Contract Level Yield system, a network of contracts maximizing the highest (and securely reliable) yield for stablecoins across chains, powered entirely by Chainlink</p><p>Chromion hackathon: YieldCoin is powered by Contract Level Yield, a system that rebalances stablecoin deposits to maximize yield as securely and reliably as possible, powered entirely by Chainlink</p><h3>Waiting for Loggo (log trigger auto access)</h3><p>there is not much time left to get a polished submission (and fix the precision loss bug)</p><p>lets assume that chainlink wont grant access to log trigger automation in time, so i need to:</p><ol><li>get the encrypted secrets working with eth sepolia</li><li>deploy infra with parent on eth sepolia</li><li>README</li><li>frontend</li><li>demo video</li><li>submit by 29th</li></ol><h3>Encrypted secrets on eth sepolia</h3><p>(for the purpose of using log trigger automation because relying on chainlink to grant access in time is not a good way to work)</p><p>i need</p><ul><li>basic CLF request contract</li><li>rerun encryption scripts with eth sepolia values</li></ul><p>ok that worked first time. nice.</p><p>Eth encrypted secret is:</p><pre>Encrypted Secrets (hex): 0x519ffb85c8bd90ed9f422f733082d43d03332a97459dec67e5922c64af61f22c8e0f872f324c884dc55d40cade8d28e111668f614eedb48adb5519e6f1c59cdea63470052378d9d51609209505c061e5c7d5c9a703b1d27f40697c944672b6d917ad539cc5bb5da1d1de79cc4508f4c82a3fb2cd8c8f20589039a372bc419ecdda561f94472033e3fd7d43610d639592653576f506c1d9185077ac3533333998538cfb478885da3ced87752b5bb213adb918d58791e083c59d94a580f2f568c2c35345815d3722f4675f18e4286a851f757e3f8582078c321dbf1193e30112540182260fef5bf97b08ab5565a0de2d0fc3</pre><pre>s_encryptedSecret =<br>            hex&quot;519ffb85c8bd90ed9f422f733082d43d03332a97459dec67e5922c64af61f22c8e0f872f324c884dc55d40cade8d28e111668f614eedb48adb5519e6f1c59cdea63470052378d9d51609209505c061e5c7d5c9a703b1d27f40697c944672b6d917ad539cc5bb5da1d1de79cc4508f4c82a3fb2cd8c8f20589039a372bc419ecdda561f94472033e3fd7d43610d639592653576f506c1d9185077ac3533333998538cfb478885da3ced87752b5bb213adb918d58791e083c59d94a580f2f568c2c35345815d3722f4675f18e4286a851f757e3f8582078c321dbf1193e30112540182260fef5bf97b08ab5565a0de2d0fc3&quot;;<br>    }</pre><p>alright what do we need now?</p><p>That encrypted secret is in ParentCLF</p><pre>== Return ==<br>0: contract Share 0x2Ed4E8435eFf62Eb48E613159a6a5Fe86b19fa16<br>1: contract SharePool 0x2891C37D5104446d10dc29eA06c25C6f0cA233Ec<br>2: contract ParentCLF 0xAF1B97Bb3f6809813a50cB99c892872b3F0B6EFb<br>3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3<br>4: uint64 5026<br>Error: `Unknown1` is above the contract size limit (25081 &gt; 24576).<br>Do you wish to continue? no<br>Error: User canceled the script.</pre><p>sigh.</p><p>0: contract ParentRebalancer 0x2Ed4E8435eFf62Eb48E613159a6a5Fe86b19fa16</p><p>oof</p><pre>== Return ==<br>0: contract Share 0x2891C37D5104446d10dc29eA06c25C6f0cA233Ec<br>1: contract SharePool 0x9bf12E915461A48bc61ddca5f295A0E20BBBa5D7<br>2: contract ParentCLF 0xc19688E191dEB933B99cc78D94c227784c8062F9<br>3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3<br>4: uint64 5026<br>Error: `Unknown1` is above the contract size limit (24836 &gt; 24576).<br>Do you wish to continue? [y/n]</pre><p>Error: Unknown1 is above the contract size limit (24786 &gt; 24576).</p><p>slowly getting there….</p><p>Error: Unknown1 is above the contract size limit (24776 &gt; 24576).</p><p>…</p><p>Error: Unknown1 is above the contract size limit (24907 &gt; 24576).</p><p>HOW??</p><p>Error: Unknown1 is above the contract size limit (24776 &gt; 24576).</p><p>ok getting rid of the try catch from the ccipReceive fixed this.</p><ol><li>deploy ParentRebalancer on Eth sepolia</li><li>update config</li><li>deploy ParentCLF</li><li>verify contracts on block explorer</li><li>update config constants</li><li>set peer on mock aave</li><li>fund with link</li><li>approve and deposit usdc</li><li>add CLF consumer</li><li>register time based automation</li><li>set upkeep address on parent</li><li>register log trigger automation</li><li>set forwarder on rebalancer</li><li>set parentPeer on rebalancer</li><li>deploy Child on base</li><li>verify on explorer</li><li>update config constants</li><li>set peer on mock aave</li><li>fund with link</li><li>deploy avax child too (had to change i_parentSelector)</li><li>set crosschain vars script</li></ol><h3>ParentCLF eth sepolia:</h3><p>0: contract Share 0x63B99C7a940b6bEf45BcA91cC0f211f0915a05Dd 1: contract SharePool 0x027B7015E7E6C82cEFB99829f4F6FF68C134F650 2: contract ParentCLF 0xBCa5B2baFcEeFc9e3F5aEf3c723dcDc494868dE0 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3 4: uint64 5026</p><p>verification not working..why not,….</p><p>wrong addressProvider address that’ll be why</p><p>forgot to add a setter for parentPeer in parentRebalancer. great.</p><p>mock aave pool not withdrawing usdc…… this is a hard project. im so tired. i cant wait for july.</p><p>nevermind i guess i set the wrong peer. glad i dont have to spend 40 mins deploying and setting stuff again. didnt even finish the last time</p><p>oh wait i do have to redeploy everything cus of the rebalancer thing. well at least i dont have to redeploy the mock aave. my brain. i need rest.</p><p>0: contract ParentRebalancer 0x107C9A78c447c99289B84476f53620236114AbAa</p><p>0: contract Share 0x37D13c62D2FDe4A400e2018f2fA0e3da6b15718D 1: contract SharePool 0x9CF6491ace3FDD614FB8209ec98dcF98b1e70e4D 2: contract ParentCLF 0xBE679979Eaec355d1030d6f117Ce5B4b5388318E 3: contract HelperConfig 0x5aAdFB43eF8dAF45DD80F4676345b7676f1D70e3</p><p>ok now we’re back where we were</p><h3>Base sep child:</h3><p>0: contract Share 0x2DF8c615858B479cBC3Bfef3bBfE34842d7AaA90 1: contract SharePool 0xEF13904800eFA60BB1ea5f70645Fc55609F00320 2: contract ChildPeer 0x94563Bfe55D8Df522FE94e7D60D2D949ef21BF1c</p><h3>Ava fuji child:</h3><p>0: contract Share 0x2891C37D5104446d10dc29eA06c25C6f0cA233Ec 1: contract SharePool 0x9bf12E915461A48bc61ddca5f295A0E20BBBa5D7 2: contract ChildPeer 0xc19688E191dEB933B99cc78D94c227784c8062F9</p><h3>Moment of truth</h3><p>everything is set (i hope) now we wait for the cron job to hopefully trigger CLF which (if better strategy is on different chain) will trigger loggo to ccip rebalance…..</p><p>we need to wait 10 minutes. it took like an hour and a half just to deploy and set up all this stuff.</p><p>my script on playground is returning base on compound so as long as that doesnt change 10 mins from now we will HOPEFULLY see CLA trigger CLF trigger CLA trigger CCIP aave on eth → compound on base</p><p>which is the most important functionality for the entire system</p><p>if this gets nailed, i can integrate testnet contracts with frontend, finish that, readme and demo video, and diagrams. we’re close but there’s still a long way to go.</p><p>im bracing for this not working. no way will it work.</p><p>AAAAAAAND nothing happened.</p><p>the time based automation did nothing.</p><p>oh wait it was a little bit late</p><p>time based auto worked <a href="https://sepolia.etherscan.io/tx/0xc8159327d9c76b118c2caa10c9db513cc38c2c7a00e3c2f026df12d2b5e6190a">https://sepolia.etherscan.io/tx/0xc8159327d9c76b118c2caa10c9db513cc38c2c7a00e3c2f026df12d2b5e6190a</a></p><p>clf request worked <a href="https://sepolia.etherscan.io/tx/0x2521aea1c73c8ace2b5630b74c60857788944479e8dcd8a7a8362a74f8970a8b">https://sepolia.etherscan.io/tx/0x2521aea1c73c8ace2b5630b74c60857788944479e8dcd8a7a8362a74f8970a8b</a></p><p>log trigger auto worked <a href="https://sepolia.etherscan.io/tx/0x1099dbd2cd04403635b820cd17508aa7c56929bc99187b39a543a7b36cd50e4d">https://sepolia.etherscan.io/tx/0x1099dbd2cd04403635b820cd17508aa7c56929bc99187b39a543a7b36cd50e4d</a></p><p>ccip rebalance sending….. expected: 21 mins <a href="https://ccip.chain.link/#/side-drawer/msg/0xb01894363f416f83171ee994cd043eacf4cc487bc2d8a589229d02c2649ed10b">https://ccip.chain.link/#/side-drawer/msg/0xb01894363f416f83171ee994cd043eacf4cc487bc2d8a589229d02c2649ed10b</a></p><p>wew please god let this work</p><p>success….</p><p>dst tx: <a href="https://sepolia.basescan.org/tx/0x35f97388d654b63d80f4d9b88eab11fb4ee16a909862dd19338c8a758565a70c">https://sepolia.basescan.org/tx/0x35f97388d654b63d80f4d9b88eab11fb4ee16a909862dd19338c8a758565a70c</a></p><p>it didnt deposit to compound, but it did deposit to aave. i guess the strategy changed between my script but hey it worked</p><p>it’s alive. IT’S ALIVE. IT FINALLY WORKS. phew that was tough</p><p>right what needs to be done now?</p><ol><li>finish frontend</li><li>integrate onchain testnet contracts</li><li>deploy on <a href="http://contractlevel.com">contractlevel.com</a></li><li>readme</li><li>diagrams</li><li>demo video</li><li>submit</li><li>precision loss bug (will probably last til end of hackathon)</li><li>fees</li><li>additional strategies</li><li>svm compatability</li></ol><h3>Deposit tx from avalanche (chain c) → parent (eth) → strategy (base)</h3><p>deposit tx: <a href="https://testnet.snowtrace.io/tx/0x68b8118e9e9115e8f8956cc05edc06d8fe281f0955a762c830d98a7f87230a06?chainid=43113">https://testnet.snowtrace.io/tx/0x68b8118e9e9115e8f8956cc05edc06d8fe281f0955a762c830d98a7f87230a06?chainid=43113</a></p><p>deposit to parent: <a href="https://ccip.chain.link/#/side-drawer/msg/0x2a996da193b64a4c4c719921655e5fe57d8292914a48572cfafec02c5349bfc7">https://ccip.chain.link/#/side-drawer/msg/0x2a996da193b64a4c4c719921655e5fe57d8292914a48572cfafec02c5349bfc7</a></p><p>dst tx: <a href="https://sepolia.etherscan.io/tx/0x6685ae8f7c883ab2f83ea43afe838f51b1b8270eab16ebb26cc1782012766fc4">https://sepolia.etherscan.io/tx/0x6685ae8f7c883ab2f83ea43afe838f51b1b8270eab16ebb26cc1782012766fc4</a></p><p>deposit to parent and deposit to strategy: <a href="https://ccip.chain.link/tx/0x6685ae8f7c883ab2f83ea43afe838f51b1b8270eab16ebb26cc1782012766fc4">https://ccip.chain.link/tx/0x6685ae8f7c883ab2f83ea43afe838f51b1b8270eab16ebb26cc1782012766fc4</a></p><p>strategy chain deposit: <a href="https://sepolia.basescan.org/tx/0x75e0f2ec96dde84126c8ec36f1bc5467c69bdb0b41e5c211e8ab99c65189baa3">https://sepolia.basescan.org/tx/0x75e0f2ec96dde84126c8ec36f1bc5467c69bdb0b41e5c211e8ab99c65189baa3</a></p><p>deposit callback parent: <a href="https://ccip.chain.link/tx/0x75e0f2ec96dde84126c8ec36f1bc5467c69bdb0b41e5c211e8ab99c65189baa3">https://ccip.chain.link/tx/0x75e0f2ec96dde84126c8ec36f1bc5467c69bdb0b41e5c211e8ab99c65189baa3</a></p><p>parent callback:</p><p><a href="https://sepolia.etherscan.io/tx/0x905c386823c1bceeb07a51c4d67effff82f8db7e1d16f2349fe2ffd053263f8f">https://sepolia.etherscan.io/tx/0x905c386823c1bceeb07a51c4d67effff82f8db7e1d16f2349fe2ffd053263f8f</a></p><p>deposit callback child: <a href="https://ccip.chain.link/tx/0x905c386823c1bceeb07a51c4d67effff82f8db7e1d16f2349fe2ffd053263f8f">https://ccip.chain.link/tx/0x905c386823c1bceeb07a51c4d67effff82f8db7e1d16f2349fe2ffd053263f8f</a></p><p>final tx minting yieldcoin/shares based on totalValue from strategy chain and totalShares from parent chain: <a href="https://testnet.snowtrace.io/tx/0x4c02081f317a22bc7c2d2768ae8e2e1144e0ad0b36a605fc2158a5b34d903123">https://testnet.snowtrace.io/tx/0x4c02081f317a22bc7c2d2768ae8e2e1144e0ad0b36a605fc2158a5b34d903123</a></p><h3>Withdraw tx from chain c (avalanche) → parent (eth) → strategy (base)</h3><p>withdraw initiate with transferAndCall: <a href="https://testnet.snowtrace.io/tx/0x1c635d115f41651df0bb29559629e30e82ec8e51f564d73d2bba0a564d8efb0b?chainid=43113">https://testnet.snowtrace.io/tx/0x1c635d115f41651df0bb29559629e30e82ec8e51f564d73d2bba0a564d8efb0b?chainid=43113</a></p><p>withdraw to parent: <a href="https://ccip.chain.link/#/side-drawer/msg/0xc8ebdd6da9a925a7b7e24001f1fc95b8bb650ebee3cbe1cbb9135ed68240d9e7">https://ccip.chain.link/#/side-drawer/msg/0xc8ebdd6da9a925a7b7e24001f1fc95b8bb650ebee3cbe1cbb9135ed68240d9e7</a></p><p>parent tx where shares are updated: <a href="https://sepolia.etherscan.io/tx/0xd6c19a86d0afbd1367cfff0262be838cbfdcf87356767c3b272b0a447269667f">https://sepolia.etherscan.io/tx/0xd6c19a86d0afbd1367cfff0262be838cbfdcf87356767c3b272b0a447269667f</a></p><p>withdraw to strategy: <a href="https://ccip.chain.link/tx/0xd6c19a86d0afbd1367cfff0262be838cbfdcf87356767c3b272b0a447269667f#/side-drawer/msg/0xef446fc7fba9cb80ac96fc5fdc69f00fce8a374991828949cdd673373a8bb31b">https://ccip.chain.link/tx/0xd6c19a86d0afbd1367cfff0262be838cbfdcf87356767c3b272b0a447269667f#/side-drawer/msg/0xef446fc7fba9cb80ac96fc5fdc69f00fce8a374991828949cdd673373a8bb31b</a></p><p>withdraw from strategy: <a href="https://sepolia.basescan.org/tx/0x67271c1cf24250bb942c4e3bc3179ecda9b5bdaa46bda7671a3b4b9415953f70">https://sepolia.basescan.org/tx/0x67271c1cf24250bb942c4e3bc3179ecda9b5bdaa46bda7671a3b4b9415953f70</a></p><p>withdraw callback:</p><p><a href="https://ccip.chain.link/tx/0x67271c1cf24250bb942c4e3bc3179ecda9b5bdaa46bda7671a3b4b9415953f70#/side-drawer/msg/0x1e5b3ddf52d453d81d4e1c0ec3c0532c90de025391a7f10b483f3c1083b497a0">https://ccip.chain.link/tx/0x67271c1cf24250bb942c4e3bc3179ecda9b5bdaa46bda7671a3b4b9415953f70#/side-drawer/msg/0x1e5b3ddf52d453d81d4e1c0ec3c0532c90de025391a7f10b483f3c1083b497a0</a></p><p>withdraw success:</p><p><a href="https://testnet.snowtrace.io/tx/0xbf9a7952bfda2561dcc92e07fe0ca58fd50bc2e88f2920fc9f22a0e96f394162">https://testnet.snowtrace.io/tx/0xbf9a7952bfda2561dcc92e07fe0ca58fd50bc2e88f2920fc9f22a0e96f394162</a></p><h3>Rebalance old strategy</h3><p>time based auto: <a href="https://sepolia.etherscan.io/tx/0x6c497c76167540e9dadded43c58d2ac29e6ace5a3ed052fcdffa6d9a454e209e">https://sepolia.etherscan.io/tx/0x6c497c76167540e9dadded43c58d2ac29e6ace5a3ed052fcdffa6d9a454e209e</a></p><p>clf callback: <a href="https://sepolia.etherscan.io/tx/0x3329aa6610892afcb93ac78d45f0a6b728daa68de4cb4e4920c76a31a5fae7bd">https://sepolia.etherscan.io/tx/0x3329aa6610892afcb93ac78d45f0a6b728daa68de4cb4e4920c76a31a5fae7bd</a></p><h3>Contract addresses:</h3><h3>Eth Sepolia</h3><p>ParentRebalancer: <a href="https://sepolia.etherscan.io/address/0x107C9A78c447c99289B84476f53620236114AbAa#code">https://sepolia.etherscan.io/address/0x107C9A78c447c99289B84476f53620236114AbAa#code</a></p><p>ParentCLF: <a href="https://sepolia.etherscan.io/address/0xBE679979Eaec355d1030d6f117Ce5B4b5388318E#code">https://sepolia.etherscan.io/address/0xBE679979Eaec355d1030d6f117Ce5B4b5388318E#code</a></p><p>YieldCoin/share token: <a href="https://sepolia.etherscan.io/address/0x37D13c62D2FDe4A400e2018f2fA0e3da6b15718D#code">https://sepolia.etherscan.io/address/0x37D13c62D2FDe4A400e2018f2fA0e3da6b15718D#code</a></p><p>SharePool (YieldCoin CCIP pool): <a href="https://sepolia.etherscan.io/address/0x9CF6491ace3FDD614FB8209ec98dcF98b1e70e4D#code">https://sepolia.etherscan.io/address/0x9CF6491ace3FDD614FB8209ec98dcF98b1e70e4D#code</a></p><h3>Base Sepolia</h3><p>Child: <a href="https://sepolia.basescan.org/address/0x94563Bfe55D8Df522FE94e7D60D2D949ef21BF1c#code">https://sepolia.basescan.org/address/0x94563Bfe55D8Df522FE94e7D60D2D949ef21BF1c#code</a></p><p>YieldCoin/share token: <a href="https://sepolia.basescan.org/address/0x2DF8c615858B479cBC3Bfef3bBfE34842d7AaA90#code">https://sepolia.basescan.org/address/0x2DF8c615858B479cBC3Bfef3bBfE34842d7AaA90#code</a></p><p>SharePool (YieldCoin CCIP pool): <a href="https://sepolia.basescan.org/address/0xEF13904800eFA60BB1ea5f70645Fc55609F00320#code">https://sepolia.basescan.org/address/0xEF13904800eFA60BB1ea5f70645Fc55609F00320#code</a></p><h3>Avalanche Fuji</h3><p>Child: <a href="https://testnet.snowtrace.io/address/0xc19688E191dEB933B99cc78D94c227784c8062F9/contract/43113/code">https://testnet.snowtrace.io/address/0xc19688E191dEB933B99cc78D94c227784c8062F9/contract/43113/code</a></p><p>YieldCoin/share token: <a href="https://testnet.snowtrace.io/address/0x2891C37D5104446d10dc29eA06c25C6f0cA233Ec/contract/43113/code">https://testnet.snowtrace.io/address/0x2891C37D5104446d10dc29eA06c25C6f0cA233Ec/contract/43113/code</a></p><p>SharePool (YieldCoin CCIP pool): <a href="https://testnet.snowtrace.io/address/0x9bf12E915461A48bc61ddca5f295A0E20BBBa5D7/contract/43113/code">https://testnet.snowtrace.io/address/0x9bf12E915461A48bc61ddca5f295A0E20BBBa5D7/contract/43113/code</a></p><h3>Subscriptions:</h3><p>Time based Automation: <a href="https://automation.chain.link/sepolia/31117210370906282501102204715287401599839953899288328641123779399532585169445">https://automation.chain.link/sepolia/31117210370906282501102204715287401599839953899288328641123779399532585169445</a></p><p>Functions: <a href="https://functions.chain.link/sepolia/5026#/side-drawer/request/0x80b52e0d902531797ec402eed7dde17f5a3b141de683da3575075785ea3c548a">https://functions.chain.link/sepolia/5026#/side-drawer/request/0x80b52e0d902531797ec402eed7dde17f5a3b141de683da3575075785ea3c548a</a></p><p>Log trigger auto: <a href="https://automation.chain.link/sepolia/58209442837245803699033621562586311107604180040493444241355605315377029566350">https://automation.chain.link/sepolia/58209442837245803699033621562586311107604180040493444241355605315377029566350</a></p><p>Frontend v0.dev: [REDACTED]</p><h3>Frontend test txs:</h3><p>deposit from parent eth to strategy base: <a href="https://ccip.chain.link/#/side-drawer/msg/0x4a87ee13ba09e2fc250d96a4c476e03d98f302e375a1b259a48a82bc156a3816">https://ccip.chain.link/#/side-drawer/msg/0x4a87ee13ba09e2fc250d96a4c476e03d98f302e375a1b259a48a82bc156a3816</a></p><h3>Next steps</h3><ol><li>finish listing all working deployed contract addresses above</li><li>test rebalance old strategy ccipTx</li><li>integrate contracts with frontend</li><li>frontend background image?</li><li>bridge functionality — the frontend will likely have to build the ccip message</li><li>readme</li><li>diagrams</li><li>demo video</li><li>ai dramatic cinematic trailer?</li><li>successful tests</li><li>frontend demo</li><li>slideshow presentation</li></ol><h3>Frontend bridge functionality</h3><p>ok lets get the frontend to move YieldCoin between chains, so i guess we need it to build a ccip message in the background and use native so dont have to approve link for spending. will probably have to approve the router to spend the yieldcoin because didnt implement transferAndCall for this on the current deployments</p><pre>/// @dev build CCIP message<br>Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);<br>tokenAmounts[0] = Client.EVMTokenAmount({token: address(yieldCoin), amount: tokenAmount});<br>yieldCoin.approve(ccipRouter, tokenAmount);<br>Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({<br>            receiver: abi.encode(msg.sender),<br>            data: &quot;&quot;,<br>            tokenAmounts: tokenAmounts,<br>            extraArgs: Client._argsToBytes(Client.GenericExtraArgsV2({gasLimit: 3000000, allowOutOfOrderExecution: true})),<br>            feeToken: address(0)<br>});<br>IRouterClient(ccipRouter).ccipSend(destinationChainSelector, evm2AnyMessage);</pre><p>Yieldcoin bridge tx (executed from frontend) <a href="https://ccip.chain.link/tx/0xd0c3e338c66bad81412c92ad7b76681b977464fa85350201b9830bfaf5250956#/side-drawer/msg/0x7f91c48fe14b5d9c6f472afa45551be29d4ff930e51711c99c8e61a980f0ed58">https://ccip.chain.link/tx/0xd0c3e338c66bad81412c92ad7b76681b977464fa85350201b9830bfaf5250956#/side-drawer/msg/0x7f91c48fe14b5d9c6f472afa45551be29d4ff930e51711c99c8e61a980f0ed58</a></p><blockquote>“You know what sucks? Having to do things to make money. YieldCoin solves that by using Chainlink to make money for you.”</blockquote><p>i need to clean my laptop its got smudges all over</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z2-EinBEYVEYtMZ9mn8WRw.png" /></figure><p>wew this readme is going to be huge</p><p>i think i need to break up what i need for the README and the demo video.</p><h3>Demo Video</h3><ul><li>frontend demo</li><li>tests passing</li><li>certora</li><li>pitch deck presentation</li><li>brand animation</li></ul><h3>ReadMe</h3><ul><li>intro</li><li>table of contents</li><li>overview</li><li>yieldcoin</li><li>strategy</li><li>contracts/architecture</li><li>yieldpeer</li><li>libraries</li><li>ccip</li><li>protocol operations</li><li>data structures</li><li>iyieldpeer</li><li>parent peer</li><li>parent clf</li><li>parent rebalancer</li><li>child peer</li><li>chainlink integrations</li><li>automation</li><li>time-based</li><li>log-trigger</li><li>functions</li><li>src.js (src.min.js)</li><li>proxy api</li><li>encrypted secrets</li></ul><h3>video pitch deck</h3><p>chrome</p><p>finish readme</p><p>check ai credits for demo video and frontend</p><h3>IMPORTANT NOTE: AT VERY END, CHECK README CODEBASE LINKS HAVENT CHANGED</h3><p>ie</p><pre>See [Parent Withdraw](&lt;https://github.com/contractlevel/yield/blob/b9f9ae814852bf2744fbede6ca4fda29d69ef7e3/src/peers/ParentPeer.sol#L119-L180&gt;) and [Child Withdraw](&lt;https://github.com/contractlevel/yield/blob/b9f9ae814852bf2744fbede6ca4fda29d69ef7e3/src/peers/ChildPeer.sol#L75-L95&gt;).</pre><p>even if this project doesn’t win a prize, it’s difficult to think of a better opportunity to spend one’s time than focusing on Chainlink Automation, Functions, CCIP, stablecoins, invariant testing, and Certora formal verification.</p><p>many github repos proudly announce they are “built with ❤” — without getting into the validity of such claims, this project definitely wasn’t. this project was built with frustration, stress, intense time management, and towards the end, satisfaction.</p><p>“one click highest yield”</p><p>unit tests need to be fixed, some of them arent passing. the readme is done for a first draft, took like 7 hours.</p><h3>Submission todo list:</h3><ol><li>fix unit tests and invariants</li><li>get full coverage on all new code</li><li>demo video</li><li>run test suite</li><li>run certora</li><li>finish pitch deck</li><li>female ai voice</li><li>record voice for frontend demo</li></ol><h3>review</h3><p>test_mockAaveNoYield_supply_updatesBalance</p><p>Encountered 6 failing tests in test/unit/withdraw/ChildWithdraw.t.sol:ChildWithdrawTest</p><p>test_yield_child_onTokenTransfer_revertsWhen_withdrawChainNotAllowed</p><p>test_yield_child_withdraw_strategyIsChainC_aave</p><p>test_yield_child_withdraw_strategyIsChainC_compound</p><p>test_yield_child_withdraw_strategyIsChild_aave</p><p>test_yield_child_withdraw_strategyIsChild_aave_withdrawToDifferentChain</p><p>test_yield_child_withdraw_strategyIsChild_compound</p><p>Encountered 2 failing tests in test/unit/withdraw/ParentWithdraw.t.sol:ParentWithdrawTest</p><p>test_yield_parent_withdraw_strategyIsNotParent_aave</p><p>test_yield_parent_withdraw_strategyIsNotParent_compound</p><p>Encountered 2 failing tests in test/unit/deposit/ParentDeposit.t.sol:ParentDepositTest</p><p>test_yield_parent_deposit_strategyIsChild_aave</p><p>test_yield_parent_deposit_strategyIsChild_compound</p><p>Encountered 4 failing tests in test/unit/deposit/ChildDeposit.t.sol:ChildDepositTest</p><p>test_yield_child_deposit_strategyIsChainC_aave</p><p>test_yield_child_deposit_strategyIsChainC_compound</p><p>test_yield_child_deposit_strategyIsChild_aave</p><p>test_yield_child_deposit_strategyIsChild_compound</p><p>i imagine this is happening because some previous functionality was commented out and replaced with the rebalancer (i was rushing to get a working testnet demo for submission and so didnt refactor any tests)</p><p>lets go through these 1 by 1</p><p>i uncommented the old code that was replaced with the rebalancer and all tests are passing again, so yeah of course it was that</p><p>this invariant is breaking though invariant_withdrawCompleted_shareBurnUpdate_consistency</p><p>well it just did 3000 runs and passed. irritating</p><p>i think i need a break before refactoring unit tests. the readme was mentally tiring</p><h4>Deployed Rebalancer: 0x107C9A78c447c99289B84476f53620236114AbAa</h4><h3>YieldCoin</h3><h3>Introduction</h3><p>YieldCoin is an automated, crosschain, stablecoin yield optimizer.</p><p>It’s built with Chainlink Automation, Functions, and CCIP.</p><p>Provides users with the highest yield across chains.</p><p>“One click highest yield”</p><h3>Problem Statement</h3><ul><li>Monitoring opportunities, withdrawing, bridging, and depositing is time consuming (sucks)</li><li>“I want to be able to click one button, and for my stablecoins to securely earn the highest reliable yield.”</li></ul><h3>Solution</h3><p>YieldCoin abstracts all of that away, giving users the simplest experience possible to earn the highest yield on their stablecoins</p><h3>How does it work?</h3><p>Chainlink Time-based Automation sends a request through Chainlink Functions to get the chain and protocol with the highest APY,</p><p>then Chainlink Log-trigger Automation uses CCIP to move funds to where it can earn the highest yield, based on the Chainlink Functions fulfilled request (callback)</p><h3>YieldCoin</h3><p>In exchange for depositing stablecoins, users receive YieldCoin</p><ul><li>represents their share of the total value in the system</li></ul><p>(total value == total deposits + generated yield)</p><ul><li>follows ERC677 and CCT standards</li></ul><h3>Smart Contracts</h3><h3>YieldPeers deployed on every chain</h3><ul><li>Users can deposit and withdraw on any chain</li><li>Interact with Yield generating strategy protocols (ie Aave, Compound)</li><li>Communicate via CCIP</li></ul><h3>Future Developments and Roadmap</h3><ul><li>support for additional stablecoins (automatic swapping to stablecoins with highest yield)</li><li>expansion to more chains</li><li>integration of more yield strategies</li><li>implementation of fees (and automated chainlink services payments)</li><li>SVM compatibility (to access yield opportunities on Solana with CCIP)</li><li>CCIP calldata compression (implement solady.lipZip)</li><li>DEX integration (allow users to buy yieldcoin with any asset)</li></ul><p>FINISH THAT SCRIPT ABOVE</p><p>Create presentation/pitch deck</p><h3>rebalance old strategy</h3><p>time based: <a href="https://sepolia.etherscan.io/tx/0xfb4413c2b8aeb0f2b849c2c962da3407334af3693d63b2112d005438eb1e114b">https://sepolia.etherscan.io/tx/0xfb4413c2b8aeb0f2b849c2c962da3407334af3693d63b2112d005438eb1e114b</a></p><p>functions: <a href="https://sepolia.etherscan.io/tx/0x99a22014821e742f247cfae3c66b363eae0022b1b8dd5516415ec26ee8389028">https://sepolia.etherscan.io/tx/0x99a22014821e742f247cfae3c66b363eae0022b1b8dd5516415ec26ee8389028</a></p><p>log trigger: <a href="https://sepolia.etherscan.io/tx/0x90af2045da5f9c65037f8337409cb2c369b1bfd084db965bbc5685e07bcf5d3f">https://sepolia.etherscan.io/tx/0x90af2045da5f9c65037f8337409cb2c369b1bfd084db965bbc5685e07bcf5d3f</a></p><p>rebalance old strategy: <a href="https://ccip.chain.link/#/side-drawer/msg/0x86a09a3f8c59d325703a8d7936834641fdf1b4cab3c25e1a64e38bf5e63d4210">https://ccip.chain.link/#/side-drawer/msg/0x86a09a3f8c59d325703a8d7936834641fdf1b4cab3c25e1a64e38bf5e63d4210</a></p><p>rebalance new strategy: <a href="https://ccip.chain.link/tx/0x08689dcb0cb6cc6149788a107e089d811f598ed79867536105280f01d12f8abe#/side-drawer/msg/0xe219bc33fbd5b23d4a564ca3bc0cc917be75cd52d16a850a23a85fbf619ebe86">https://ccip.chain.link/tx/0x08689dcb0cb6cc6149788a107e089d811f598ed79867536105280f01d12f8abe#/side-drawer/msg/0xe219bc33fbd5b23d4a564ca3bc0cc917be75cd52d16a850a23a85fbf619ebe86</a></p><h3>Pitch deck voice script</h3><p>Yield coin is an automated, cross-chain, stablecoin yield optimizer. Yield coin has been built with Chainlink Automation, Functions, and CCIP. And offers users the simplest possible experience to maximise their stablecoin yield, in just one click.</p><p>Here’s the problem statement: Monitoring opportunities, then manually withdrawing, bridging, and depositing is time consuming.</p><p>Yield coin solves that by abstracting it all away, enabling users to consistently and securely earn the highest yield on their stablecoins across the Web3 ecosystem, without any manual input.</p><p>So how does it work? There are two simple steps to this process. First, Chainlink time-based automation sends a request through Chainlink Functions to fetch the chain and protocol with the highest APY. Next, Chainlink log-trigger automation uses CCIP to move funds to where it can earn the most yield, based on the fulfilled Chainlink functions request.</p><p>In exchange for depositing stablecoins, users receive yield coin, a token that represents their share of the total value in the system, and follows the ERC677 and CCT standards.</p><p>A Yield Peer smart contract is deployed on each chain. These contracts allow users to deposit and withdraw their stablecoins on any chain, communicate with each other via CCIP, and interact with yield generating strategy protocols.</p><p>None of this would be possible without Chainlink’s reliable services. Automation eliminates the need for human intervention, Functions enables the secure retrieval of optimal yield data, and CCIP facilitates robust crosschain communication and value transfer.</p><p>There are a number of planned future developments, such as, support for additional stablecoins, expansion to more chains, integration of more yield strategies, implementation of fees and automation of Chainlink service payments, SVM compatability, CCIP calldata compression, and dex integration.</p><p>Thank you for your consideration and the opportunity to build with Chainlink.</p><p>Ok, so here is the frontend. These live protocol metrics, are um, demo values, heh. We’ve got a basic introduction, how it works. This page is still missing stuff, like uh.. header and footer, but to be honest.. we um.. there are still things with the actual contracts to improve, so that stuff is taking precedence. We can go into the app, and connect our… uh wallet. I will be using metamask because old habits die hard. Then we are gonna need to approve the contract to spend our USDC. We can see the transaction on the block explorer. Nice. And then we deposit…ahem… then if we go to block explorer… we can see the transaction on the CCIP explorer too. Ok whilst that’s happening, let’s see what else we’ve got. We can withdraw our stablecoin, and we can bridge yield coin with CCIP. The current testnets being used are ethereum, base and avalanche. So yep, we need to approve the CCIP router to transfer the Yield coin….and um..then we can bridge. The UI shows the transaction is now pending, and provides links to the source chain explorer, and the CCIP explorer. So the yield coin is being transferred from ethereum to, uh, avalanche. …and it was a success. On the avalanche explorer, we can see we have received yield coin. Cool. Thanks</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hrsRSyrNSlFZgNt1El2SNw.png" /></figure><p>The pitch deck video demo is done and so is the frontend demo. Next I need to clean up tests and record a bit of those passing, then I can spend the rest of next week focused on the bug. This project is coming along nicely.</p><p>the only thing in my unit coverage that i havent done is the SetCrosschain script. the problem here is it uses vm.startBroadcast and i cant do a prank during a broadcast, so i think i have to deploy all the infrastructure all over again (locally?) and then run it</p><p>i am so tired this was a tough week</p><p>i’ve reached the last week and, well 9 more days actually and im starting to think the architecture needs overhaul (parent peer split up) — this is in relation to dex/pricefeed integration</p><p>ultimately i think that the yield peers should all do protocol operations and ccip operations</p><p>parent peer should track state</p><p>parent helper/rebalancer does clf/cla</p><p>swap peer should do the dex/pricefeed (also deployed across chains)</p><p>Unpopular opinion: [REDACTED]</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/1*cSQ7gNhhQS3hDdBeGCrC_g.jpeg" /></figure><p><em>IDEA: USERS SHOULD BE ABLE TO CHOOSE THE YIELD STRATEGIES THEIR STABLECOINS ARE ALLOCATED TO — PROBABLY WOULDNT WORK BECAUSE IT’D BE A NIGHTMARE GETTING TOTAL VALUE — THINK ABOUT THIS</em></p><h3>Submission</h3><h4>The problem it solves</h4><p><em>describe what can people use it for, or how it makes existing tasks easier/safer e.t.c</em></p><p>There have been numerous times where I have held stablecoins in a wallet, on various chains, and wanted to “put them to work” to earn yield, instead of just sitting there. I end up struggling to find the time to keep up with the best (and safe) opportunities to position my stablecoins (whilst retaining full control and liquidity), then withdrawing, bridging and depositing again. This process is time consuming, burdensome, and boring.</p><p>Chainlink’s Automation, Functions, and CCIP components can be combined into the “Contract Level Yield” infrastructure, or “YieldCoin”, to abstract this process away — giving users the simplest possible experience to consistently and securely earn the highest yield on their stablecoins without any manual input.</p><p>Please see the <a href="https://github.com/contractlevel/yield?tab=readme-ov-file#yieldcoin-aka-contract-level-yield-cly">README</a> for details on the architecture, Chainlink integrations, deployment, testing, testnet transactions, and more.</p><h3>Challenges I ran into</h3><p><em>tell us about any specific bug or hurdle you ran into while building this project. how did you get over it?</em></p><p>There were many roadblocks of varying size for this submission. The most significant of which is a precision loss/calculation logic bug that became apparent during invariant testing and formal verification.</p><h4>Precision Loss/Share Calculation Logic Bug</h4><p>The invariant testing (which began after initially achieving full unit coverage) revealed cases where the amount of YieldCoin minted in exchange for USDC deposits was significantly less than it should have been.</p><p>I thought the issue was mitigated with “initial admin deposits” which would go untouched, something like <a href="https://solodit.cyfrin.io/issues/l-18-vault-is-susceptible-to-inflation-attack-by-first-depositor-pashov-audit-group-none-hyperstable_2025-02-26-markdown">dead shares inflation attack mitigation</a> because the invariant tests were then passing. (I actually need to try minting to address(0) — note to self for after this submission is done).</p><p>The issue came up again during formal verification with Certora. Even though initial admin shares were minted, there were still scenarios where the total value of the system and the total shares (YieldCoin) minted (values used to calculate the shareMintAmount of YieldCoin, in exchange for USDC deposits) resulted in 0 YieldCoin minted despite the user depositing USDC - breaking a key invariant of the system: “user’s should always be able to withdraw their deposited amount (minus any fees) shortly after depositing into the system”.</p><p>This issue has so far gone unresolved (today is 21st) because of how much a deep rabbit hole it could be, I wanted to get an eligible submission done before focusing on it — because there really is no way to know what other issues would’ve appeared during the testnet deployments and transactions phase.</p><p>Other than this bug, the testnet deployments and transactions behaved as intended. All of the Chainlink integrations performed their roles correctly.</p><p>Once this submission is done, the rest of the hackathon will be spent on this bug (and hopefully have solved it by the 29th).</p><h4>Burning small amounts of shares (YieldCoin) can result in no USDC redeemed</h4><p>I don’t know how closely related the root cause of this is to the previous bug described, but it’s definitely similar, and was made apparent during testing.</p><p>The issue is this: if an amount of YieldCoin (18 decimals) worth less than the lowest possible value of USDC (6 decimals) is burned in an attempt to withdraw USDC, no USDC will be withdrawn even though the YieldCoin has been burned.</p><p>It’s debatable how critical this issue is, given the unlikelyhood of someone attempting a withdrawal of such a small amount, and ends up benefiting other YieldCoin holders. However it is still unintended behavior of the system, and results in (an insignificant) loss of value.</p><p>Further research is required — it may end up being resolved with the previous issue, or it may have a different root cause. Either way, it is a top priority once this submission is done.</p><h4>USDC chainlink-local fork</h4><p>This is actually an issue I’ve had with writing ccip tests before. The current chainlink-local ccip simulator is amazing, but unfortunately doesn’t have support for USDC — the stablecoin with the most lanes on CCIP.</p><p>To fully test the system on forked mainnets, additional functionality in the CCIPLocalSimulatorFork was required to get past the additional CCTP checks for USDC. CCTP is Circle’s crosschain infrastructure for USDC that works alongside CCIP onchain.</p><p>Ultimately the <a href="https://github.com/contractlevel/chainlink-local/blob/519e854caaf1291c03bda3928674c922195fd629/src/ccip/CCIPLocalSimulatorFork.sol#L125-L238">additional functionality</a> required was the monitoring for a CCTP event and <a href="https://getfoundry.sh/reference/cheatcodes/prank/">pranking</a> the CCTP <a href="https://github.com/circlefin/evm-cctp-contracts/blob/6e7513cdb2bee6bb0cddf331fe972600fc5017c9/src/roles/Attestable.sol#L212-L230">attesters</a>.</p><h4>Proxy API</h4><p>Information pertaining to the “strategy” with the highest yield (ie the chain and the protocol) is fetched from the <a href="https://yields.llama.fi/pools">DefiLlama API</a> which returns a HUGE response. The response was too much for Chainlink Functions, so a proxy API to filter for the relevant data was required.</p><p>I made one and deployed it to AWS Lambda. The url for the API could have been abused (unlikely for a hackathon project, but a required consideration for a production ready project) so the url had to be properly encrypted with the functions-toolkit and then stored as a <a href="https://github.com/contractlevel/yield/blob/575ee3cb5f9ae11b7921728a40e0590f678dd05c/src/peers/extensions/ParentCLF.sol#L33-L37">constant</a> in the YieldCoin FunctionsClient contract. This value needed to be different for different chains due to the chain-specific parameters required when executing the encrypted secrets process.</p><h4>Time management/knowing what to prioritize</h4><p>There were a lot of parts to this project and knowing which bit to prioritize and when was a challenge. Once the unit coverage was complete I played around with adding more yield strategies and implementing fees, before deciding to focus on invariant tests. More yield strategies wasn’t exactly essential to demonstrate the full functionality of the system itself, and implementing fees was much the same.</p><p>As security is so integral to smart contract development, I decided more testing of the system so far was a higher priority than additional features that wouldn’t showcase any more Chainlink use and could be added later.</p><p>As previously mentioned, juggling the precision loss/share calculation logic bug and getting an eligible submission done was a time management/priority challenge too.</p><p>I didn’t get to implement everything I would’ve liked due to the time constraints, but that just means this project now has a <a href="https://github.com/contractlevel/yield?tab=readme-ov-file#future-developments">roadmap of future developments</a>.</p><h4>Functions callback with max gas limit not being able to execute ccip sends</h4><p>This was a big issue that didn’t become apparent until the testnet stage. The original idea for the rebalancing process went like this: Time-based Automation sends request via Chainlink Functions to fetch yield strategy with highest APY, and then the fulfillRequest callback triggers CCIP rebalance messages.</p><p>I suspected the Functions max gas limit may have been the cause of the issue, however the Tenderly calltrace showed the transaction failing on an unrelated revert — very confusing! I ended up asking in the discord hackathon support channel and received a response which led to the confirmation of my initial suspicions.</p><p>Solving this issue required a second Automation implementation, to trigger the CCIP rebalance messaged based on the Functions request callback. This could have been done with Custom Logic Automation, but that likely would’ve meant using additional, redundant storage slots, so I opted for Log-trigger Automation. The idea for this being when a better strategy was returned by Chainlink Functions, an event detailing this would be emitted, and then Log-trigger Automation would listen for this event, and execute CCIP rebalance messages based on it.</p><p>The chain that would have required the Log-trigger Automation was Base Sepolia, because that is where the ParentPeer/CLF contract (the one that interacts with Chainlink Functions) was deployed.</p><p>Log-trigger Automation on Base Sepolia required approval from the Chainlink team, which I applied for and was granted (thanks). However efficiently using the time between needing this functionality and being granted access to it was crucial. I redeployed the entire infrastructure, so that the Parent peer contract was now on Ethereum Sepolia because Log-trigger Automation on that chain did not require preapproval. A few hours after successfully executing the full rebalance transaction, I was granted access on Base Sepolia. I appreciated the fast approval from Chainlink, but the project needed to move ahead.</p><h4>Certora Formal Verification of contracts that use Chainlink FunctionsRequest library</h4><p>The Certora prover is a formal verification tool that attempts to explore all paths of a transaction, however it has its limitations — particularly when verifying contracts with “high path count”, because it causes the prover to “timeout” and the verification job stops, incomplete.</p><p>In the past I mostly ran into this problem using it on contracts with significant assembly usage, but due to the heavy use of strings and bytes in FunctionsRequest::encodeCBOR(), the prover timed out.</p><p>This was solved by adding the <a href="https://docs.certora.com/en/latest/docs/prover/cli/options.html#nondet-difficult-funcs">nondet_difficult_funcs</a> flag when running Certora, which automatically summarized view/pure functions that were previously non-summarized and difficult for the Prover to verify.</p><h4>Yield generating strategy protocols either not working on testnet or not existing</h4><p>Aave and Compound were the protocols chosen as yield generating strategies for this initial prototype. Unfortunately they were not fully available across all required testnets, so I had to use mocks in their place on testnets. These mocks function identically to their official production equivalents, but do not actually generate any yield. They are merely used to demonstrate depositing and withdrawing from these strategies.</p><h4>DefiLlama API not providing testnet data</h4><p>Mainnet data was used for the strategy with the highest APY and testnet transactions were based on this.</p><h4>Incorrect placement of networkConfig cache before vm.startBroadcast in deploy script</h4><p>This issue caused the wrong USDC address to be set in the constructor for a testnet deployment and was fixed by moving the line caching the networkConfig to after the vm.startBroadcast.</p><h3>YieldCoin: Crosschain Stablecoin Yield Optimizer</h3><h3>Introduction to YieldCoin</h3><ul><li>Automated, crosschain, stablecoin yield optimization</li><li>Built with Chainlink Automation, Functions and CCIP</li><li>One Click Highest Yield</li></ul><h3>Problem</h3><p><strong>Monitoring opportunities, then manually withdrawing, bridging and depositing is time consuming</strong></p><h3>Solution</h3><h4>YieldCoin abstracts all of that away</h4><p>Users enjoy one-click access to maximum yields across chains and protocols, without any of the headache or time involved with manually managing positions</p><h3>How does it work?</h3><h4>Step 1:</h4><p>Chainlink Time-based Automation sends a request through Chainlink Functions to get the chain and protocol with the highest APY</p><h4>Step 2:</h4><p>Chainlink Log-trigger Automation uses CCIP to move funds to where it can earn the highest yield, based on the Chainlink Functions fulfillRequest() callback</p><h3>YieldCoin Token</h3><ul><li>In exchange for depositing stablecoins, users receive YieldCoin</li><li>Represents their share of the total value in the system</li><li>Follows ERC677 and CCT standards</li></ul><h3>Smart Contracts</h3><ul><li>YieldPeer contracts deployed on every chain</li><li>Users can deposit and withdraw stablecoins on any chain</li><li>Communicate via CCIP</li><li>Interact with yield generating strategy protocols (ie Aave, Compound)</li></ul><h3>Chainlink Integrations</h3><h4>Chainlink Automation</h4><p>Enables automated on-chain functions such as yield strategy rebalancing triggered by time schedules and blockchain event logs, ensuring responsive and timely protocol operations.</p><p>ALT: Chainlink Automation streamlines yield strategy rebalancing, allowing protocols to adjust strategies automatically based on predefined time schedules and blockchain event logs. This leads to enhanced efficiency and responsiveness in yield optimization processes.</p><h4>Chainlink Functions</h4><p>Fetches and processes offchain data from external APIs like DefiLlama via a proxy API, enabling secure retrieval of optimal stablecoin yield strategies for the system.</p><h4>Cross-Chain Interoperability Protocol (CCIP)</h4><p>Facilitates robust crosschain communication and value transfers, allowing seamless deposits, withdrawals, and yield allocations across multiple blockchains with robust decentralized security measures.</p><h3>Future Developments &amp; Roadmap</h3><h4>Support for Additional Stablecoins</h4><p>Enable automatic swapping to the stablecoin offering the highest yield for greater flexibility.</p><h4>Expansion to More Chains</h4><p>Extend multi-chain presence to increase accessibility and diversification opportunities.</p><h4>Integration of More Yield Strategies</h4><p>Incorporate additional yield-generating protocols to optimize returns and manage risk.</p><h4>Implementation of Fees</h4><p>Introduce automated fee mechanisms including Chainlink subscription top-ups for seamless service.</p><h4>SVM Compatibility</h4><p>Develop compatibility with Solana Virtual Machine to access emerging blockchain ecosystems.</p><h4>CCIP Calldata Compression</h4><p>Enhance cross-chain communication efficiency by compressing calldata.</p><h4>Dex Integration</h4><p>Enable buying YieldCoin with any asset via DEX swaps.</p><h4>Thank You</h4><p>@contractlevel</p><p>….</p><h4>CCIP Calldata Compression &amp; Dex Integration</h4><p>Enhance cross-chain communication efficiency and enable buying YieldCoin with any asset via DEX swaps.</p><p>Chromion hackathon: User deposited stablecoins are periodically rotated into the highest yield generating strategy, an automated, crosschain process, secured entirely by the combination of Chainlink services. In return for deposits, users are minted YieldCoin.</p><h3>Final week</h3><p>0xf4240 deposit amount</p><p>0xde0b6b3a7640001 total value</p><p>0xe8d4a51000 total shares</p><p>these are the amounts resulting in 0 yield coin minted</p><p>so this is still breaking</p><pre>// @review this is uncovering a critical edgecase bug and needs to be revisited.<br>rule deposit_mintsShares_when_parent_is_strategy() {<br>    env e;<br>    calldataarg args;<br>    require getStrategy().chainSelector == getThisChainSelector();<br>    <br>    uint256 shareSupplyBefore = share.totalSupply();<br>    uint256 totalSharesBefore = getTotalShares();<br>    /// @notice simulating initial admin deposit to mitigate inflation attack<br>    require getTotalValue(e) &gt;= 1000000 &amp;&amp; totalSharesBefore &gt;= 1000000000000; // 1 usdc<br>    deposit(e, args);<br>    assert share.totalSupply() &gt; shareSupplyBefore;<br>    assert getTotalShares() &gt; totalSharesBefore;<br>}</pre><p>even with something like this in the parent constructor _mintShares(address(0), 1e18);</p><p>let’s look at</p><ol><li>deposit()</li></ol><pre>/// @notice Users can deposit USDC into the system via this function<br>    /// @notice As this is the ParentPeer, we handle two deposit cases:<br>    /// 1. This Parent is the Strategy<br>    /// 2. This Parent is not the Strategy<br>    /// @param amountToDeposit The amount of USDC to deposit into the system<br>    /// @dev Revert if amountToDeposit is 0<br>    function deposit(uint256 amountToDeposit) external override {<br>        _initiateDeposit(amountToDeposit);<br>        Strategy memory strategy = s_strategy;<br>        // 1. This Parent is the Strategy. Therefore the deposit is handled here and shares can be minted here.<br>        if (strategy.chainSelector == i_thisChainSelector) {<br>            uint256 totalValue = _depositToStrategyAndGetTotalValue(amountToDeposit);<br>            uint256 shareMintAmount = _calculateMintAmount(totalValue, amountToDeposit);<br>            s_totalShares += shareMintAmount;<br>            /// @dev mint share tokens (YieldCoin) to msg.sender based on amount deposited and total value of the system<br>            _mintShares(msg.sender, shareMintAmount);<br>            emit ShareMintUpdate(shareMintAmount, i_thisChainSelector, s_totalShares);<br>        }<br>        // 2. This Parent is not the Strategy. Therefore the deposit must be sent to the strategy and get totalValue.<br>        else {<br>            DepositData memory depositData = _buildDepositData(amountToDeposit);<br>            _ccipSend(strategy.chainSelector, CcipTxType.DepositToStrategy, abi.encode(depositData), amountToDeposit);<br>            emit DepositForwardedToStrategy(amountToDeposit, strategy.chainSelector);<br>        }<br>    }</pre><ol><li>certora calltrace</li></ol><pre>require (ParentHarness.getStrategy()).chainSelector == ParentHarness.getThisChainSelector()</pre><pre>ParentHarness.getStrategy() ↪ {chainSelector=0x2711, protocol=IYieldPeer.Protocol.Aave}</pre><pre>ParentHarness.getThisChainSelector() ↪ 0x2711</pre><pre>(ParentHarness.getStrategy()).chainSelector == ParentHarness.getThisChainSelector() ↪ true</pre><p>so we’re on the right chain selector, our require is working</p><pre>ParentPeer._calculateMintAmount(totalValue=0xde0b6b3a7640001, amount=0xf4240) ↪ 0x0</pre><pre>Load from ParentHarness.s_totalShares ↪ 0xe8d4a51000</pre><pre>totalShares ↪ 0xe8d4a51000</pre><p>i dont know what those numbers are but this is very bad that there is a scenario when someone deposits an amount of stablecoin and get no yieldcoin back when there is also value in the system and other yieldcoin in existence</p><p>lets check the numbers</p><pre>uint256 totalYieldCoin = 0xe8d4a51000;<br>➜ a<br>Type: uint256<br>├ Hex: 0xe8d4a51000<br>├ Hex (full word): 0x000000000000000000000000000000000000000000000000000000e8d4a51000<br>└ Decimal: 1000000000000</pre><pre>uint256 totalValue = 0xde0b6b3a7640001;<br>➜ totalValue<br>Type: uint256<br>├ Hex: 0xde0b6b3a7640001<br>├ Hex (full word): 0x0000000000000000000000000000000000000000000000000de0b6b3a7640001<br>└ Decimal: 1000000000000000001</pre><pre>uint256 deposit = 0xf4240;<br>➜ deposit<br>Type: uint256<br>├ Hex: 0xf4240<br>├ Hex (full word): 0x00000000000000000000000000000000000000000000000000000000000f4240<br>└ Decimal: 1000000</pre><p>wew this very bad too so</p><p>when there’s</p><ul><li>1000000000000 many yield coins</li><li>1000000000000.000001 this much $ value</li><li>$1 this much deposit</li></ul><p>that $1 deposit will just disappear</p><pre>function _calculateMintAmount(uint256 totalValue, uint256 amount) internal view returns (uint256 shareMintAmount) {<br>        uint256 totalShares = s_totalShares;<br>        // @review if totalShares isn&#39;t 0, then totalValue shouldn&#39;t be either.<br>        if (totalShares == 0 || totalValue == 0) shareMintAmount = amount * INITIAL_SHARE_PRECISION;<br>        else shareMintAmount = (amount * totalShares) / totalValue;<br>    }</pre><p>the sense of who we are is about to change rapidly. humanity, identity, ai, and smart contracts. truly noone understands how bonkers this is about to get.</p><p>the rule passes with this:</p><pre>uint256 numerator = amount * totalShares;<br>        shareMintAmount = numerator / totalValue;<br>        if (numerator % totalValue != 0) {<br>            shareMintAmount += 1; // Round up if there&#39;s a remainder<br>        }</pre><p>but im not sure im satisfied with that. i had an issue on the testnet with one of the deposits worth $1 was getting less back when withdrawing. maybe problem in withdraw</p><p>will really need to verify withdrawals based on the shareBurnAmount $ worth</p><p>calculate it somehow</p><p>should each of these be using fixedpointmath?</p><pre>uint256 numerator = amount * totalShares;<br>shareMintAmount = numerator / totalValue;</pre><p>trying the solady fixedpointmathlib to see if it does anything</p><p>i still suspect the underlying issue is with the calculation itself</p><p>this is what NEEDS to happen for this protocol: when a user deposits usdc, they should ALWAYS be able to redeem that same amount (shortly after — some wei difference may occur when depositing in strategy protocols if attempting to withdraw straight after) (and minus any fees)</p><p>ok so minting dead shares in the constructor is not possible because the parent peer has not been granted a mint role for the shares until after it is deployed.</p><p>using solady.fixedpointmathlib results in a timeout cus of the assembly……….</p><p>unit tests arent passing now</p><p>trying fixpoint with nondet flag</p><p>ok well using full mul div up breaks the unit tests so cant use that</p><p>Chromion hackathon: when a user deposits stablecoins, they should ALWAYS be able to redeem that same amount (shortly after — some wei difference may occur when depositing in strategy protocols if attempting to withdraw straight after) (and minus any fees). This invariant is critical to YieldCoin</p><p>the mental process of understanding the bug’s relation to the invariant was made quicker with writing certora rules than had it been done in foundry</p><p>certora gives you more time for thinking about your system whereas with foundry invariants, so much time is spent setting up the handler, that a lot of the thinking on this side is spent on everything external to your system, then gradually progressing through the entry points</p><h3>Disclaimer</h3><p><em>These smart contracts and code are being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the user interface or the smart contracts and code. They have not been audited and as such there can be no assurance they will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. THE SMART CONTRACTS AND CODE CONTAINED HEREIN ARE FURNISHED AS IS, WHERE IS, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING ANY WARRANTY OF MERCHANTABILITY, NON-INFRINGEMENT OR FITNESS FOR ANY PARTICULAR PURPOSE. Further, use of any of these smart contracts and code may be restricted or prohibited under applicable law, including securities laws, and it is therefore strongly advised for you to contact a reputable attorney in any jurisdiction where these smart contracts and code may be accessible for any questions or concerns with respect thereto. Further, no information provided in this repo should be construed as investment advice or legal advice for any particular facts or circumstances, and is not meant to replace competent counsel. Contract Level is not liable for any use of the foregoing, and users should proceed with caution and use at their own risk.</em></p><p>there is a limit to the things we can think about, and that limit is time, the end of all things</p><p>chainlink is digital lego but maths is spiritual lego</p><p>i’ll try asking grok:</p><p>In the problematic scenario, you provided the following values:</p><ul><li>totalShares = 0xe8d4a51000 (decimal: 1,000,000,000,000 or 1e12)</li><li>totalValue = 0xde0b6b3a7640001 (decimal: 1,000,000,000,000,000,001 or 1e18 + 1)</li><li>amount = 0xf4240 (decimal: 1,000,000 or 1e6, representing 1 USDC with 6 decimals)</li></ul><p>The calculation becomes:</p><pre>shareMintAmount = (amount * totalShares) / totalValue<br>                = (1,000,000 * 1,000,000,000,000) / 1,000,000,000,000,000,001<br>                = 1,000,000,000,000,000,000 / 1,000,000,000,000,000,001<br>                ≈ 0 (due to integer division in Solidity)</pre><p>Since Solidity performs integer division and discards the remainder, the result is 0. This means a user depositing 1 USDC receives no shares, which violates your protocol’s requirement: users should always be able to redeem their deposited USDC amount (shortly after deposit, barring minor strategy-related differences or fees).</p><p>Root Cause</p><p>The issue arises because:</p><ol><li>Integer Division: When amount * totalShares is less than totalValue, the division yields a quotient less than 1, which Solidity rounds down to 0.</li><li>Precision Loss: The large difference in scale between totalShares (1e12) and totalValue (1e18 + 1), combined with a relatively small amount (1e6), exacerbates the problem, making the result vanish in integer arithmetic.</li></ol><p>This edge case is critical because it breaks the fundamental invariant of your system: a deposit should mint a proportional number of shares, ensuring users can later withdraw their funds.</p><p>what about putting invariant checks in the calculation logic?</p><p>ie</p><p>in _calculateShareMintAmount the result is compared against _calculateUsdcWithdrawAmount</p><p>maybe i should write this invariant in foundry</p><p>you know i think we’re gonna need an event for tracking usdc deposits via other chains</p><p>DepositUpdate(uint256 amount, uint64 chainSelector) - the sum of amounts emitted by this event should be the total usdc deposited across chains</p><p>WithdrawUpdate(uint256 amount, uint64 chainSelector) - the sum of amounts emitted by this event should be the total usdc withdrawn across chains</p><p>oh right i dont think we can actually write this in certora due to the crosschain nature of how state is updated in this system and how certora havocs values. this would require significant architectural additions to the certora spec</p><pre>// invariant totalValue_consistency(env e)<br>//     getStrategy().chainSelector == getThisChainSelector() =&gt; <br>//         getTotalValue(e) &gt;= ghost_totalUsdcDeposited - ghost_totalUsdcWithdrawn;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QTDNSlJgdDb0XW2lBUnVNA.png" /></figure><p>not ideal</p><pre>[0] VM::startPrank(0x00000000000000004D672A669bC7E8E68E958068)<br>    │   └─ ← [Return]<br>    ├─ [24686] MockUsdc::approve(ChildPeer: [0xc7183455a4C133Ae270771860664b6B7ec320bB1], 781452050345 [7.814e11])<br>    │   ├─ emit Approval(owner: 0x00000000000000004D672A669bC7E8E68E958068, spender: ChildPeer: [0xc7183455a4C133Ae270771860664b6B7ec320bB1], value: 781452050345 [7.814e11])<br>    │   ├─  storage changes:<br>    │   │   @ 0x98e02edba7e97f9fb0987e2108e274778fabe1dd804a067e29568f6c02a57774: 0 → 0x000000000000000000000000000000000000000000000000000000b5f22bffa9<br>    │   └─ ← [Return] true<br>    ├─ [301302] ChildPeer::deposit(781452050345 [7.814e11])<br>    │   ├─ [25832] MockUsdc::transferFrom(0x00000000000000004D672A669bC7E8E68E958068, ChildPeer: [0xc7183455a4C133Ae270771860664b6B7ec320bB1], 781452050345 [7.814e11])<br>    │   │   ├─ emit Transfer(from: 0x00000000000000004D672A669bC7E8E68E958068, to: ChildPeer: [0xc7183455a4C133Ae270771860664b6B7ec320bB1], value: 781452050345 [7.814e11])</pre><p>so this guy is depositing 781452.050345 USDC and getting 0 shares minted in return. very bad.</p><p>thats with this in mint calc</p><pre>uint256 numerator = amount * totalShares;<br>        shareMintAmount = (numerator + totalValue - 1) / totalValue; // Ceiling division</pre><p>ok i got past that bit</p><p>if (totalShares == 0) { _mintShares(address(1), 1e18); totalShares = 1e18; // was missing this line } uint256 numerator = amount * totalShares; shareMintAmount = (numerator + totalValue — 1) / totalValue; // Ceiling division</p><p>this is where we are now</p><p>still dont know if this is the right direction. im just kinda aiming in the dark right now</p><p>im pretty sure that logic is not where we want to be going</p><p>i think i should go through the invariant calltrace, perhaps add some console logs for deposits and withdraws</p><h3>TODO:</h3><ol><li>bug</li></ol><p>this didnt work:</p><pre>// ------------------------------------------------------------//<br>        /// @notice rule passes with this, but im not satisfied with it<br>        // if (totalShares == 0 || totalValue == 0) {<br>        //     shareMintAmount = amount * INITIAL_SHARE_PRECISION;<br>        // } else {<br>        //     uint256 numerator = amount * totalShares;<br>        //     shareMintAmount = numerator / totalValue;<br>        //     if (numerator % totalValue != 0) {<br>        //         shareMintAmount += 1; // Round up if there&#39;s a remainder<br>        //     }<br>        // }<br>        // ------------------------------------------------------------//<br>        /// @notice this breaks the unit tests<br>        // FixedPointMathLib.fullMulDivUp(amount, totalShares, shareMintAmount);<br>        // shareMintAmount = FixedPointMathLib.fullMulDivUp(amount, totalShares, totalValue);<br>        // ------------------------------------------------------------//<br>        // if (totalShares == 0 || totalValue == 0) {<br>        //     shareMintAmount = amount * INITIAL_SHARE_PRECISION; // e.g., 1e18 for initial mint<br>        // } else {<br>        //     uint256 numerator = amount * totalShares;<br>        //     shareMintAmount = (numerator + totalValue - 1) / totalValue; // Ceiling division<br>        // }<br>        // ------------------------------------------------------------//<br>        // if (totalShares == 0) {<br>        //     _mintShares(address(1), 1e18);<br>        //     totalShares = 1e18;<br>        // }<br>        // uint256 numerator = amount * totalShares;<br>        // shareMintAmount = (numerator + totalValue - 1) / totalValue; // Ceiling division<br>        // ------------------------------------------------------------//<br>        // // uint256 totalShares = s_totalShares;<br>        // uint256 virtualShares = 1e12; // Small virtual shares pool<br>        // uint256 virtualAssets = 1e6; // 1 USDC<br>        // if (totalShares == 0) {<br>        //     // Initial case: no admin mint needed, rely on virtual shares<br>        //     totalShares = virtualShares;<br>        //     totalValue += virtualAssets;<br>        // } else {<br>        //     totalValue += virtualAssets;<br>        //     totalShares += virtualShares;<br>        // }<br>        // uint256 numerator = amount * totalShares;<br>        // shareMintAmount = (numerator + totalValue - 1) / totalValue; // Ceiling division<br>        //     // s_totalShares += shareMintAmount; // Update storage</pre><p>i’ve added some decimal conversion logic and that isnt fixing the issue</p><pre>/// @param totalValue The total value of the system to 6 decimals<br>    /// @param amount The amount of USDC deposited<br>    /// @return shareMintAmount The amount of SHAREs to mint<br>    /// @notice Returns amount * (SHARE_DECIMALS / USDC_DECIMALS) if there are no shares minted yet<br>    function _calculateMintAmount(uint256 totalValue, uint256 amount) internal view returns (uint256 shareMintAmount) {<br>        uint256 totalShares = s_totalShares;<br>        // @review if totalShares isn&#39;t 0, then totalValue shouldn&#39;t be either.<br>        // if (totalShares != 0) shareMintAmount = (amount * totalShares) / totalValue;<br>        // else shareMintAmount = amount * INITIAL_SHARE_PRECISION;<br>        // ------------------------------------------------------------<br>        // uint256 crossChainBalanceConverted = _convertToLPTokenDecimals(totalCrossChainLiquidity);<br>        // uint256 amountDepositedConverted = _convertToLPTokenDecimals(_amountToDeposit);<br>        // return<br>        //     (((crossChainBalanceConverted + amountDepositedConverted) * _totalLPSupply) /<br>        //         crossChainBalanceConverted) - _totalLPSupply;<br>        // ------------------------------------------------------------<br>        if (totalShares == 0) {<br>            shareMintAmount = amount * INITIAL_SHARE_PRECISION;<br>        } else {<br>            uint256 totalValueConverted = _convertUsdcToShare(totalValue);<br>            uint256 amountDepositedConverted = _convertUsdcToShare(amount);<br>            shareMintAmount =<br>                (((totalValueConverted + amountDepositedConverted) * totalShares) / totalValueConverted) - totalShares;<br>        }<br>    }</pre><pre>/// @notice Helper function to calculate the USDC withdraw amount<br>    /// @param totalValue The total value in the Contract Level Yield system<br>    /// @param totalShares The total shares in the Contract Level Yield system<br>    /// @param shareBurnAmount The amount of shares the withdrawer burned<br>    /// @return usdcWithdrawAmount The USDC withdraw amount<br>    // @review should we be converting decimals here?<br>    function _calculateWithdrawAmount(uint256 totalValue, uint256 totalShares, uint256 shareBurnAmount)<br>        internal<br>        pure<br>        returns (uint256 usdcWithdrawAmount)<br>    {<br>        // usdcWithdrawAmount = (shareBurnAmount * totalValue) / totalShares;<br>        // ------------------------------------------------------------<br>        /**<br>         * function _calculateWithdrawableAmount(<br>         *     uint256 _childPoolsBalance,<br>         *     uint256 _clpAmount,<br>         *     uint256 _lpSupply<br>         * ) internal view returns (uint256) {<br>         *     uint256 parentPoolLiquidity = i_USDC.balanceOf(address(this)) +<br>         *         s_loansInUse +<br>         *         s_depositsOnTheWayAmount -<br>         *         s_depositFeeAmount;<br>         *     uint256 totalCrossChainLiquidity = _childPoolsBalance + parentPoolLiquidity;<br>         *<br>         *     //USDC_WITHDRAWABLE = POOL_BALANCE x (LP_INPUT_AMOUNT / TOTAL_LP)<br>         *     uint256 amountUsdcToWithdraw = (((_convertToLPTokenDecimals(totalCrossChainLiquidity) *<br>         *         _clpAmount) * PRECISION_HANDLER) / _lpSupply) / PRECISION_HANDLER;<br>         *<br>         *     return _convertToUSDCTokenDecimals(amountUsdcToWithdraw);<br>         * }<br>         */<br>        uint256 shareWithdrawAmount =<br>            (((_convertUsdcToShare(totalValue) * shareBurnAmount) * 1e18) / totalShares) / 1e18;<br>        usdcWithdrawAmount = _convertShareToUsdc(shareWithdrawAmount);<br>    }<br>    function _convertUsdcToShare(uint256 amountInUsdc) internal pure returns (uint256 amountInShare) {<br>        amountInShare = amountInUsdc * 1e12;<br>    }<br>    function _convertShareToUsdc(uint256 amountInShare) internal pure returns (uint256 amountInUsdc) {<br>        // amountInUsdc = amountInShare / INITIAL_SHARE_PRECISION;<br>        amountInUsdc = (amountInShare * 1e6) / 1e18;<br>    }</pre><p>total value 0xde0b6b3a7640001</p><p>deposit amount 0xf4240</p><p>total shares 0xe8d4a51000</p><p>cant believe still stuck on this</p><p>this results in 0 shares minted.</p><p>im trying to do a basic unit test just to see if a third depositor can withdraw their amount</p><pre>function test_withdrawIntegrity_multipleUsers() public {<br>        address user1 = makeAddr(&quot;user1&quot;);<br>        address user2 = makeAddr(&quot;user2&quot;);<br>        address user3 = makeAddr(&quot;user3&quot;);<br>        deal(address(baseUsdc), user1, DEPOSIT_AMOUNT);<br>        deal(address(baseUsdc), user2, DEPOSIT_AMOUNT);<br>        deal(address(baseUsdc), user3, DEPOSIT_AMOUNT);<br>        _changePrank(user1);<br>        baseUsdc.approve(address(baseParentPeer), DEPOSIT_AMOUNT);<br>        baseParentPeer.deposit(DEPOSIT_AMOUNT);<br>        uint256 expectedShareBalance = DEPOSIT_AMOUNT * INITIAL_SHARE_PRECISION;<br>        assertEq(baseShare.balanceOf(user1), expectedShareBalance);<br>        _changePrank(user2);<br>        baseUsdc.approve(address(baseParentPeer), DEPOSIT_AMOUNT);<br>        baseParentPeer.deposit(DEPOSIT_AMOUNT);<br>        _changePrank(user3);<br>        baseUsdc.approve(address(baseParentPeer), DEPOSIT_AMOUNT);<br>        baseParentPeer.deposit(DEPOSIT_AMOUNT);<br>        uint256 shareBalance = baseShare.balanceOf(user3);<br>        baseShare.transferAndCall(address(baseParentPeer), shareBalance, &quot;&quot;);<br>        assertEq(baseUsdc.balanceOf(user3), DEPOSIT_AMOUNT);<br>    }</pre><p>[71371] Share::transferAndCall(ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809], 499999999833333333388 [4.999e20], 0x) │ ├─ emit Transfer(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], to: ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809], value: 499999999833333333388 [4.999e20]) │ ├─ emit Transfer(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], to: ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809], value: 499999999833333333388 [4.999e20], data: 0x) │ ├─ [42773] ParentCLF::onTokenTransfer(user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], 499999999833333333388 [4.999e20], 0x) │ │ ├─ emit SharesBurned(from: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 499999999833333333388 [4.999e20]) │ │ ├─ [5100] Share::burn(499999999833333333388 [4.999e20]) │ │ │ ├─ emit Transfer(from: ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809], to: 0x0000000000000000000000000000000000000000, value: 499999999833333333388 [4.999e20]) │ │ │ └─ ← [Stop] │ │ ├─ emit ShareBurnUpdate(shareBurnAmount: 499999999833333333388 [4.999e20], chainSelector: 15971525489660198786 [1.597e19], totalShares: 1500000000000000000000 [1.5e21]) │ │ ├─ emit WithdrawInitiated(withdrawer: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 499999999833333333388 [4.999e20], thisChainSelector: 15971525489660198786 [1.597e19]) │ │ ├─ [469] 0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D::getPool() [staticcall] │ │ │ └─ ← [Return] 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 │ │ ├─ [5223] 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5::getReserveData(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) [staticcall] │ │ │ ├─ [4540] 0x6EF6B6176091F94A8aD52C08e571F81598b226A2::getReserveData(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) [delegatecall] │ │ │ │ ├─ [546] 0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D::getAddress(0x4d4f434b5f535441424c455f4445425400000000000000000000000000000000) [staticcall] │ │ │ │ │ └─ ← [Return] 0xaED3b56FeA82E809665f02AcBcDEc0816c75f4d9 │ │ │ │ └─ ← [Return] ReserveData({ configuration: ReserveConfigurationMap({ data: 7237005577332262213973186568752004950073150606504593676736434993981981990220 [7.237e75] }), liquidityIndex: 1093298078607243051141944794 [1.093e27], currentLiquidityRate: 38446223936621012526437616 [3.844e25], variableBorrowIndex: 1126994287780805171877382416 [1.126e27], currentVariableBorrowRate: 52358795901662810505078528 [5.235e25], currentStableBorrowRate: 0, lastUpdateTimestamp: 1750706507 [1.75e9], id: 4, aTokenAddress: 0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB, stableDebtTokenAddress: 0xaED3b56FeA82E809665f02AcBcDEc0816c75f4d9, variableDebtTokenAddress: 0x59dca05b6c26dbd64b5381374aAaC5CD05644C28, interestRateStrategyAddress: 0x86AB1C62A8bf868E1b3E1ab87d587Aba6fbCbDC5, accruedToTreasury: 1039981578 [1.039e9], unbacked: 0, isolationModeTotalDebt: 0 }) │ │ │ └─ ← [Return] ReserveData({ configuration: ReserveConfigurationMap({ data: 7237005577332262213973186568752004950073150606504593676736434993981981990220 [7.237e75] }), liquidityIndex: 1093298078607243051141944794 [1.093e27], currentLiquidityRate: 38446223936621012526437616 [3.844e25], variableBorrowIndex: 1126994287780805171877382416 [1.126e27], currentVariableBorrowRate: 52358795901662810505078528 [5.235e25], currentStableBorrowRate: 0, lastUpdateTimestamp: 1750706507 [1.75e9], id: 4, aTokenAddress: 0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB, stableDebtTokenAddress: 0xaED3b56FeA82E809665f02AcBcDEc0816c75f4d9, variableDebtTokenAddress: 0x59dca05b6c26dbd64b5381374aAaC5CD05644C28, interestRateStrategyAddress: 0x86AB1C62A8bf868E1b3E1ab87d587Aba6fbCbDC5, accruedToTreasury: 1039981578 [1.039e9], unbacked: 0, isolationModeTotalDebt: 0 }) │ │ ├─ [3334] 0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB::balanceOf(ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809]) [staticcall] │ │ │ ├─ [2729] 0x98F409Fc4A42F34AE3c326c7f48ED01ae8cAeC69::balanceOf(ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809]) [delegatecall] │ │ │ │ ├─ [1450] 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5::getReserveNormalizedIncome(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) [staticcall] │ │ │ │ │ ├─ [845] 0x6EF6B6176091F94A8aD52C08e571F81598b226A2::getReserveNormalizedIncome(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) [delegatecall] │ │ │ │ │ │ └─ ← [Return] 1093298078607243051141944794 [1.093e27] │ │ │ │ │ └─ ← [Return] 1093298078607243051141944794 [1.093e27] │ │ │ │ └─ ← [Return] 3000000001 [3e9] │ │ │ └─ ← [Return] 3000000001 [3e9] │ │ ├─ [469] 0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D::getPool() [staticcall] │ │ │ └─ ← [Return] 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 │ │ ├─ [15987] 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5::withdraw(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, 750000000062499999983 [7.5e20], ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809]) │ │ │ ├─ [15363] 0x6EF6B6176091F94A8aD52C08e571F81598b226A2::withdraw(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, 750000000062499999983 [7.5e20], ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809]) [delegatecall] │ │ │ │ ├─ [2577] 0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D::getPriceOracle() [staticcall] │ │ │ │ │ └─ ← [Return] 0x2Cc0Fc26eD4563A5ce5e8bdcfe1A2878676Ae156 │ │ │ │ ├─ [6588] SupplyLogic::186dea44(00000000000000000000000000000000000000000000000000000000000000340000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000003734f9527f09ec799b9026a09dda4338c753ef75fd021c6ce2cfc8bc6671f628fd000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000028a8574262f44250ef000000000000000000000000deb1e9a6be7baf84208bb6e10ac9f9bbe1d70809000000000000000000000000000000000000000000000000000000000000000d0000000000000000000000002cc0fc26ed4563a5ce5e8bdcfe1a2878676ae1560000000000000000000000000000000000000000000000000000000000000000) [delegatecall] │ │ │ │ │ ├─ [1021] 0x59dca05b6c26dbd64b5381374aAaC5CD05644C28::scaledTotalSupply() [staticcall] │ │ │ │ │ │ ├─ [419] 0x2425A746911128c2eAA7bEBDc9Bc452eE52208a1::scaledTotalSupply() [delegatecall] │ │ │ │ │ │ │ └─ ← [Return] 0x000000000000000000000000000000000000000000000000000072a1de61950e │ │ │ │ │ │ └─ ← [Return] 0x000000000000000000000000000000000000000000000000000072a1de61950e │ │ │ │ │ ├─ [1296] 0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB::scaledBalanceOf(ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809]) [staticcall] │ │ │ │ │ │ ├─ [691] 0x98F409Fc4A42F34AE3c326c7f48ED01ae8cAeC69::scaledBalanceOf(ParentCLF: [0xDEb1E9a6Be7Baf84208BB6E10aC9F9bbE1D70809]) [delegatecall] │ │ │ │ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000000000a38dfa88 │ │ │ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000000000a38dfa88 │ │ │ │ │ └─ ← [Revert] 32</p><p><a href="https://github.com/concero/contracts-v1/blob/master/contracts/ParentPoolCommon.sol">https://github.com/concero/contracts-v1/blob/master/contracts/ParentPoolCommon.sol</a></p><p><a href="https://github.com/concero/contracts-v1/blob/master/contracts/ParentPool.sol">https://github.com/concero/contracts-v1/blob/master/contracts/ParentPool.sol</a></p><p>_calculateLPTokensToMint</p><p>_calculateWithdrawableAmount</p><p>im following the logic from these exactly</p><p>right now im depositing the same usdc amount from 3 users</p><p>user 1 gets:</p><p>emit SharesMinted(to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 1000000000000000000000 [1e21])</p><p>user2:</p><p>emit SharesMinted(to: user2: [0x537C8f3d3E18dF5517a58B3fB9D9143697996802], amount: 500000000000000000000 [5e20])</p><p>user3:</p><p>emit SharesMinted(to: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 499999999833333333388 [4.999e20])</p><p>be aware of:</p><p>ParentPeer::_calculateMintAmount()</p><p>YieldPeer::_calculateWithdrawAmount()</p><p>YieldPeer::_depositToStrategyAndGetTotalValue:</p><pre>/// @notice Deposits USDC to the strategy and returns the total value of the system<br>    /// @param amount The amount of USDC to deposit<br>    /// @return totalValue The total value of the system<br>    function _depositToStrategyAndGetTotalValue(uint256 amount) internal returns (uint256 totalValue) {<br>        address strategyPool = _getStrategyPool();<br>        // @review swapped these lines to get totalValue first<br>        _depositToStrategy(strategyPool, amount);<br>        totalValue = _getTotalValueFromStrategy(strategyPool);<br>        // @review this event<br>        // is this event only emitted when a user deposits, and not a rebalance?<br>        // we should have some rebalance events<br>        emit DepositCompleted(strategyPool, amount, totalValue);<br>    }</pre><p>if these lines are in this order:</p><pre>_depositToStrategy(strategyPool, amount);<br>totalValue = _getTotalValueFromStrategy(strategyPool);</pre><p>then invariant_totalValue_integrity passes</p><p>if they are swapped (which seems better for share mint amount calc)</p><p>the invariant fails because total usdc withdrawn ends up being more than deposited</p><p>and on a locally mocked environment with no actual yield generation, the total usdc withdrawn should not be more than the total usdc deposited</p><p>if those lines get swapped around, then test_withdrawIntegrity_multipleUsers passes, still not sure about the aprox eq though</p><p>also test_yield_parent_deposit_multipleDeposits logic was changed in here, it now works for those 2 lines getting the value before depositing,</p><p>but maybe i should just change it so that the calc func uses totalValue — amount instead of totalValue</p><pre>if (totalShares != 0) {<br>            shareMintAmount = <br>(_convertUsdcToShare(amount) * totalShares) <br>/ <br>_convertUsdcToShare(totalValue - amount);</pre><p>i changed the totalValue to totalValue — amount and the invariant fails like that too. its</p><p>i forge cleaned lets see if it happens again</p><p>It’s a bit alarming [REDACTED]</p><p>[24561] Invariant::invariant_totalValue_integrity() ├─ [19306] Handler::forEachChainSelector(0x7fa9385be102ac3eac297483dd6233d62b3e14969b981774) │ ├─ [13994] Invariant::checkTotalValuePerChainSelector(1) │ │ ├─ [2674] ParentCLF::getStrategy() [staticcall] │ │ │ └─ ← [Return] Strategy({ chainSelector: 1, protocol: 0 }) │ │ ├─ [2350] Handler::ghost_event_totalUsdcWithdrawn() [staticcall] │ │ │ └─ ← [Return] 1000135990584 [1e12] │ │ ├─ [2372] Handler::ghost_state_totalUsdcDeposited() [staticcall] │ │ │ └─ ← [Return] 1000099004420 [1e12] │ │ └─ ← [Revert] panic: arithmetic underflow or overflow (0x11)</p><p>how is this caused by subtracting the deposit amount from the totalValue in mint calc?</p><p>coasting off past glories is a one way ticket to stagnation. nobody cares what you achieved yesterday.</p><p>[56349] Invariant::invariant_stablecoinRedemptionIntegrity() ├─ [51101] Handler::forEachUser(0x7fa9385be102ac3eac297483dd6233d62b3e14963cb6a470) │ ├─ [45776] Invariant::checkRedemptionIntegrityPerUser(0x00175C08A08D6247FF697b2532C5ceDe3b60489B) │ │ ├─ [2577] Handler::ghost_state_totalUsdcDepositedPerUser(0x00175C08A08D6247FF697b2532C5ceDe3b60489B) [staticcall] │ │ │ └─ ← [Return] 895935326107 [8.959e11] │ │ ├─ [2554] Handler::ghost_event_totalUsdcWithdrawnPerUser(0x00175C08A08D6247FF697b2532C5ceDe3b60489B) [staticcall] │ │ │ └─ ← [Return] 895935326105 [8.959e11] │ │ ├─ [2580] Share::balanceOf(0x00175C08A08D6247FF697b2532C5ceDe3b60489B) [staticcall] │ │ │ └─ ← [Return] 1 │ │ ├─ [2674] ParentCLF::getStrategy() [staticcall] │ │ │ └─ ← [Return] Strategy({ chainSelector: 1, protocol: 1 }) │ │ ├─ [2552] Handler::chainSelectorsToPeers(1) [staticcall] │ │ │ └─ ← [Return] ParentCLF: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a] │ │ ├─ [13257] ParentCLF::getTotalValue() [staticcall] │ │ │ ├─ [7431] MockComet::balanceOf(ParentCLF: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) [staticcall] │ │ │ │ └─ ← [Return] 1281036577119 [1.281e12] │ │ │ └─ ← [Return] 1281036577119 [1.281e12] │ │ ├─ [2380] ParentCLF::getTotalShares() [staticcall] │ │ │ └─ ← [Return] 1280159578194766111579939 [1.28e24] │ │ ├─ [380] ParentCLF::getTotalShares() [staticcall] │ │ │ └─ ← [Return] 1280159578194766111579939 [1.28e24] │ │ ├─ [0] VM::assertTrue(false, “Invariant violated: User should be able to withdraw what they deposited”) [staticcall] │ │ │ └─ ← [Revert] Invariant violated: User should be able to withdraw what they deposited</p><p>(runs: 1299, calls: 19485, reverts: 0)</p><p>quite a few runs, definitely making small progress</p><p>[2577] Handler::ghost_state_totalUsdcDepositedPerUser(0x00175C08A08D6247FF697b2532C5ceDe3b60489B) [staticcall] │ │ │ └─ ← [Return] 895935326107 [8.959e11] │ │ ├─ [2554] Handler::ghost_event_totalUsdcWithdrawnPerUser(0x00175C08A08D6247FF697b2532C5ceDe3b60489B) [staticcall] │ │ │ └─ ← [Return] 895935326105 [8.959e11] │ │ ├─ [2580] Share::balanceOf(0x00175C08A08D6247FF697b2532C5ceDe3b60489B) [staticcall] │ │ │ └─ ← [Return] 1</p><p>these values are very close. maybe i should account for slippage</p><pre>function invariant_stablecoinRedemptionIntegrity() public {<br>        handler.forEachUser(this.checkRedemptionIntegrityPerUser);<br>    }<br><br>function checkRedemptionIntegrityPerUser(address user) external view {<br>        uint256 deposited = handler.ghost_state_totalUsdcDepositedPerUser(user);<br>        uint256 withdrawn = handler.ghost_event_totalUsdcWithdrawnPerUser(user);<br>        uint256 netDeposits = deposited &gt; withdrawn ? deposited - withdrawn : 0;<br>        uint256 userShares = share.balanceOf(user);<br>        uint256 totalValue =<br>            IYieldPeer(handler.chainSelectorsToPeers(parent.getStrategy().chainSelector)).getTotalValue();<br>        if (parent.getTotalShares() &gt; 0) {<br>            uint256 withdrawable = (userShares * totalValue) / parent.getTotalShares();<br>            uint256 minWithdrawable = netDeposits * 99 / 100; // Allow 1% slippage<br>            assertTrue(<br>                withdrawable &gt;= minWithdrawable,<br>                &quot;Invariant violated: User should be able to withdraw what they deposited&quot;<br>            );<br>        } else {<br>            assertTrue(netDeposits == 0, &quot;Invariant violated: User should be able to withdraw what they deposited&quot;);<br>        }<br>    }</pre><p>oh i already am.</p><p>[Return] 1351340984566 [1.351e12] │ │ ├─ [2554] Handler::ghost_event_totalUsdcWithdrawnPerUser(0x00000000000000000007289B355b95C50D2A6bB6) [staticcall] │ │ │ └─ ← [Return] 1351340984564 [1.351e12] │ │ ├─ [2580] Share::balanceOf(0x00000000000000000007289B355b95C50D2A6bB6) [staticcall] │ │ │ └─ ← [Return] 3</p><p>(runs: 2267, calls: 34005, reverts: 0)</p><p>lets try again with conversions and console logs:</p><pre>function checkRedemptionIntegrityPerUser(address user) external view {<br>        uint256 deposited = handler.ghost_state_totalUsdcDepositedPerUser(user);<br>        uint256 withdrawn = handler.ghost_event_totalUsdcWithdrawnPerUser(user);<br>        uint256 netDeposits = deposited &gt; withdrawn ? deposited - withdrawn : 0;<br>        uint256 userShares = share.balanceOf(user)<br>        uint256 totalValue =<br>            IYieldPeer(handler.chainSelectorsToPeers(parent.getStrategy().chainSelector)).getTotalValue();<br>        uint256 totalValueConverted = _convertUsdcToShare(totalValue);<br>        if (parent.getTotalShares() &gt; 0) {<br>            uint256 withdrawable = (userShares * totalValueConverted) / parent.getTotalShares();<br>            uint256 withdrawableConverted = _convertShareToUsdc(withdrawable);<br>            uint256 minWithdrawable = netDeposits * 999 / 1000; // Allow 1% slippage<br>            if (withdrawableConverted &lt; minWithdrawable) {<br>                console2.log(&quot;User:&quot;, user);<br>                console2.log(&quot;Deposited:&quot;, deposited);<br>                console2.log(&quot;Withdrawn:&quot;, withdrawn);<br>                console2.log(&quot;Net Deposits:&quot;, netDeposits);<br>                console2.log(&quot;User Shares:&quot;, userShares);<br>                console2.log(&quot;Total Value:&quot;, totalValue);<br>                console2.log(&quot;Total Shares:&quot;, totalShares);<br>                console2.log(&quot;Withdrawable:&quot;, withdrawable);<br>                console2.log(&quot;Min Withdrawable:&quot;, minWithdrawable);<br>            }<br>            assertTrue(<br>                withdrawableConverted &gt;= minWithdrawable,<br>                &quot;Invariant violated: User should be able to withdraw what they deposited&quot;<br>            );<br>        } else {<br>            assertTrue(netDeposits == 0, &quot;Invariant violated: User should be able to withdraw what they deposited&quot;);<br>        }<br>    }</pre><p>[77933] Invariant::invariant_stablecoinRedemptionIntegrity() ├─ [72685] Handler::forEachUser(0x7fa9385be102ac3eac297483dd6233d62b3e14963cb6a470) │ ├─ [67360] Invariant::checkRedemptionIntegrityPerUser(0xFfFfFfFFfFFFFfffffFfFfFffffffFFfFFFfFFfc) │ │ ├─ [2577] Handler::ghost_state_totalUsdcDepositedPerUser(0xFfFfFfFFfFFFFfffffFfFfFffffffFFfFFFfFFfc) [staticcall] │ │ │ └─ ← [Return] 1000000047700 [1e12] │ │ ├─ [2554] Handler::ghost_event_totalUsdcWithdrawnPerUser(0xFfFfFfFFfFFFFfffffFfFfFffffffFFfFFFfFFfc) [staticcall] │ │ │ └─ ← [Return] 1000000047698 [1e12] │ │ ├─ [2580] Share::balanceOf(0xFfFfFfFFfFFFFfffffFfFfFffffffFFfFFFfFFfc) [staticcall] │ │ │ └─ ← [Return] 3 │ │ ├─ [2674] ParentCLF::getStrategy() [staticcall] │ │ │ └─ ← [Return] Strategy({ chainSelector: 1, protocol: 0 }) │ │ ├─ [2552] Handler::chainSelectorsToPeers(1) [staticcall] │ │ │ └─ ← [Return] ParentCLF: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a] │ │ ├─ [25666] ParentCLF::getTotalValue() [staticcall] │ │ │ ├─ [154] MockPoolAddressesProvider::getPool() [staticcall] │ │ │ │ └─ ← [Return] MockAavePool: [0x5366ba85D9F7FF951548a0B990E6cc4e56f5A976] │ │ │ ├─ [3955] MockAavePool::getReserveData(MockUsdc: [0x40b4863C923385D1632d640B097DDeA6EaB92e76]) [staticcall] │ │ │ │ └─ ← [Return] ReserveData({ configuration: ReserveConfigurationMap({ data: 0 }), liquidityIndex: 0, currentLiquidityRate: 0, variableBorrowIndex: 0, currentVariableBorrowRate: 0, currentStableBorrowRate: 0, lastUpdateTimestamp: 0, id: 0, aTokenAddress: 0xA8156Cbca2b79bb568d251F44eF88b9EcAF697C4, stableDebtTokenAddress: 0x0000000000000000000000000000000000000000, variableDebtTokenAddress: 0x0000000000000000000000000000000000000000, interestRateStrategyAddress: 0x0000000000000000000000000000000000000000, accruedToTreasury: 0, unbacked: 0, isolationModeTotalDebt: 0 }) │ │ │ ├─ [8291] MockAToken::balanceOf(ParentCLF: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) [staticcall] │ │ │ │ ├─ [7531] MockAavePool::balanceOf(ParentCLF: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a]) [staticcall] │ │ │ │ │ └─ ← [Return] 100000002 [1e8] │ │ │ │ └─ ← [Return] 100000002 [1e8] │ │ │ └─ ← [Return] 100000002 [1e8] │ │ ├─ [2380] ParentCLF::getTotalShares() [staticcall] │ │ │ └─ ← [Return] 100000000000000000003 [1e20] │ │ ├─ [380] ParentCLF::getTotalShares() [staticcall] │ │ │ └─ ← [Return] 100000000000000000003 [1e20] │ │ ├─ [0] console::log(“User:”, 0xFfFfFfFFfFFFFfffffFfFfFffffffFFfFFFfFFfc) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] console::log(“Deposited:”, 1000000047700 [1e12]) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] console::log(“Withdrawn:”, 1000000047698 [1e12]) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] console::log(“Net Deposits:”, 2) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] console::log(“User Shares:”, 3) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] console::log(“Total Value:”, 100000002 [1e8]) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [380] ParentCLF::getTotalShares() [staticcall] │ │ │ └─ ← [Return] 100000000000000000003 [1e20] │ │ ├─ [0] console::log(“Total Shares:”, 100000000000000000003 [1e20]) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] console::log(“Withdrawable:”, 3) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] console::log(“Min Withdrawable:”, 1) [staticcall] │ │ │ └─ ← [Stop] │ │ ├─ [0] VM::assertTrue(false, “Invariant violated: User should be able to withdraw what they deposited”) [staticcall] │ │ │ └─ ← [Revert] Invariant violated: User should be able to withdraw what they deposited</p><p>does this mean theres something wrong with my min withdrawable calc</p><p>ok this invariant is passing when allowing dust</p><p>time to fix the totalValue invariant</p><p>well how is this happening</p><p>Handler::ghost_event_totalUsdcWithdrawn() [staticcall] │ │ │ └─ ← [Return] 1000135986976 [1e12] │ │ ├─ [2372] Handler::ghost_state_totalUsdcDeposited() [staticcall] │ │ │ └─ ← [Return] 1000099000812</p><p>the total withdrawn is more than the total deposited? this is impossible because we arent generating any yield from local deployments</p><p>this invariant was passing when the total value in the mint calc included the deposit amount</p><p>Logs: total deposited: 100000000 shareMintAmount: 100000000000000000000 Bound result 96979600946188821296820120169 Bound result 999999007606 Bound result 2 depositor: 0x0000000000000001395ba54DA1bb07301C85eA69 depositAmount: 999999007606 total deposited: 1000099007606 shareMintAmount: 999999007606000000000000 Bound result 3 Bound result 0 Bound result 0 Bound result 999999007606000000000000 Bound result 3 Bound result 2 withdrawer: 0x0000000000000001395ba54DA1bb07301C85eA69 shareBurnAmount: 999999007606000000000000 usdc withdraw amount: 1000135993771 total withdrawn: 1000135993771</p><p>how are we withdrawing more than we deposited (when no yield is being generated)</p><p>the shareBurnAmount is the same as the shareMintAmount so clearly there is some sort of discrepancy between the calculations for minting and withdrawing</p><p>stablecoin redemption integrity passes with this withdraw logic:</p><pre>shareWithdrawAmount = <br>(((_convertUsdcToShare(totalValue) * shareBurnAmount) * 1e18) <br>/ totalShares) / 1e18;<br>usdcWithdrawAmount = _convertShareToUsdc(shareWithdrawAmount);</pre><p>total value integrity passes with this:</p><pre>(((totalValue * _convertShareToUsdc(shareBurnAmount)) * 1e18) <br>/ _convertUsdcToShare(totalShares)) / 1e18;</pre><p>the invariants need to pass with the same logic</p><p>Logs: total deposited: 100.000000 shareMintAmount: 100000000000000000000 depositor: 0x0000000000000000000000000000000000000016 depositAmount: 999999.018749 total deposited: 1000099.018749 shareMintAmount: 999999.018749000000000000 withdrawer: 0x0000000000000000000000000000000000000016 shareBurnAmount: 13506 usdc withdraw amount: 0 total withdrawn: 0 withdrawer: 0x0000000000000000000000000000000000000016 shareBurnAmount: 999999.018748999999986493 usdc withdraw amount: 1000136.004915 total withdrawn: 1000136.004915 totalValue 100.013699 deposited 1000099.018749 withdrawn 1000136.004915</p><p>Logs: total deposited: 100000000 shareMintAmount: 100000000000000000000 Bound result 1159529525519484852166244857971432224692093622042 Bound result 999999009307 Bound result 3 depositor: 0xCb1B1a292645f197a1786771c6A613545550971A depositAmount: 999999009307 total deposited: 1000099009307 shareMintAmount: 999999009307000000000000 Bound result 3 Bound result 1 Bound result 0 Bound result 999999009306999999999998 Bound result 2 Bound result 3 inside _calculateWithdrawAmount totalValue 1000236009171 shareBurnAmount 999999009306999999999998 totalShares 1000099009307000000000000 shareWithdrawAmount 1000135995472369891873852 usdcWithdrawAmount 1000135995472 withdrawer: 0xCb1B1a292645f197a1786771c6A613545550971A shareBurnAmount: 999999009306999999999998 usdc withdraw amount: 1000135995472 total withdrawn: 1000135995472 totalValue 100013699 deposited 1000099009307 withdrawn 1000135995472</p><p>emit DepositCompleted(strategyPool: MockPoolAddressesProvider: [0x794aC86b74c89e4c4f5722e8cdB15e982ecD1C92], amount: 712274906849 [7.122e11], totalValue: 712374906849 [7.123e11])</p><p>[0] console::log(“inside _calculateWithdrawAmount”) [staticcall] │ │ │ │ │ │ │ └─ ← [Stop] │ │ │ │ │ │ ├─ [0] console::log(“totalValue”, 712570091423 [7.125e11]) [staticcall] │ │ │ │ │ │ │ └─ ← [Stop] │ │ │ │ │ │ ├─ [0] console::log(“shareBurnAmount”, 712274906848999999999999 [7.122e23]) [staticcall] │ │ │ │ │ │ │ └─ ← [Stop] │ │ │ │ │ │ ├─ [0] console::log(“totalShares”, 712374906849000000000000 [7.123e23]) [staticcall] │ │ │ │ │ │ │ └─ ← [Stop] │ │ │ │ │ │ ├─ [0] console::log(“shareWithdrawAmount”, 712470064023863376372028 [7.124e23]) [staticcall] │ │ │ │ │ │ │ └─ ← [Stop] │ │ │ │ │ │ ├─ [0] console::log(“usdcWithdrawAmount”, 712470064023 [7.124e11]) [staticcall]</p><p>712374906849, 712570091423 why is total value changing here?????</p><p>MockComet::balanceOf(ChildPeer: [0xc7183455a4C133Ae270771860664b6B7ec320bB1]) [staticcall] │ │ │ │ │ │ │ └─ ← [Return] 712570091423 [7.125e11]</p><p>oh its because i actually am applying an interest rate on the mock comet</p><pre>function _calculateInterest(address account) internal view returns (uint256) {<br>        uint256 balance = s_balances[account];<br>        if (balance == 0) return 0;<br>        uint256 timeElapsed = block.timestamp - s_lastUpdateTimestamp[account];<br>        // Calculate interest: balance * rate * time / (365 days * 10000)<br>        return (balance * s_interestRate * timeElapsed) / (365 days * 10000);<br>    }</pre><p>wew what a huge waste of time.</p><p>so what the only thing we got from all this was the totalValue including amount prior to being used for share mint calc was a bug</p><p>theres still precision edge cases uncovered with certora, but at least users get the correct yieldcoin amount for their stablecoin</p><p>great tests arent passing now</p><p>great even more tests failing now</p><p>why didnt i commit</p><p>ALWAYS COMMIT WHEN TESTS PASS</p><p>this is very bad.</p><p>i dont know what i’ve done</p><p>i dont know how to undo this</p><p>i tried fixing the totalValue — amount thing across all instances</p><p>that caused tests to fail</p><p>i tried to undo it</p><p>that caused more tests to fail</p><p>i tried to undo more</p><p>tests still failing</p><p>i shouldve committed when they all passed</p><p>i was converting the total value before subtracting the amount by mistake</p><p>alright back to the edgecase……..</p><p>depositAmount 0xf4240–1000000</p><p>depositAmountConvertedToShare: 0xde0b6b3a7640000–1000000000000000000</p><p>totalValue: 0x56bc75e2d631f4241–100000000000001000001</p><p>totalValueConvertedToShare: 0x4ee2d6d415b85acf069d4a51000–100000000000000000001000000000000</p><p>totalShares: 0x5af3107a4000–100000000000000</p><p>humanity is being held back by [REDACTED]</p><h3>Edgecase</h3><p>When someone deposits 1 USDC (1.000000) and there are already 100000000000001.000001 USDC in the system and 100000000000000</p><p>wait why is total shares only 0.000100000000000000 yield coin??</p><pre>uint256 shareSupplyBefore = share.totalSupply();<br> uint256 totalSharesBefore = getTotalShares();<br> <br> require getTotalValue(e) &gt;= 100000000 &amp;&amp; <br>	 totalSharesBefore &gt;= 100000000000000;<br> require share.balanceOf(0) == 10000000000000000000;</pre><pre>    deposit(e, args);</pre><p>total value should be at least 100 USDC for initial admin deposit</p><p>total shares should be whatever that is * 1e12</p><p>usd = 100000000</p><p>shares = 100000000000000000000</p><p>failing</p><p>totalShares 0x56bc75e2d63100000 100.000000000000000000</p><p>totalValue 0x52b7d2dcc80cd2e40f4241 100000000000000000001.000001</p><p>deposit 0xf4240 1.000000</p><p>there should never be a case when someone deposits any amount of usdc and gets 0 yieldcoin back</p><p>the rule passes if the required values are == to those admin values</p><p>but if they are ≥ it fails with the edgecases.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FnBRV753QD4Y%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DnBRV753QD4Y&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FnBRV753QD4Y%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/3cefaf9242277598cd49dc8781fbe00c/href">https://medium.com/media/3cefaf9242277598cd49dc8781fbe00c/href</a></iframe><h3>Thing to do before hackathon end</h3><ol><li>fix video (its got a black bar at the top, probably doesnt matter)</li><li>finish all @review comments</li><li>refactor rebalancer tests for full rebalancer flow</li><li>would need to refactor invariants</li><li>formally verify rebalancer</li></ol><p>im gonna mitigate the edgecase by rounding up 0 to 1. simple</p><p>i verified that the edge case mitigation withdrawal can withdraw 1 usdc. however surely this means that if someone deposits the smallest value of usdc in this instance, they would be able to withdraw 1 usdc in exchange? well we are enforcing a minimum deposit of 1 usdc…..so perhaps that is already mitigated</p><p>alright mitigated. let’s think about what needs to happen in the next few days for this hackathon submission to be where it should be.</p><ol><li>refactor rebalancer tests for full flow</li><li>refactor invariants</li><li>finish @review comments</li><li>natspec everywhere</li></ol><p>ok these are the unit tests that fail when commenting out the old logic the rebalancer replaces:</p><p>Failing tests: Encountered 4 failing tests in <strong>test/unit/deposit/ChildDeposit.t.sol:ChildDepositTest</strong> test_yield_child_deposit_strategyIsChainC_aave test_yield_child_deposit_strategyIsChainC_compound test_yield_child_deposit_strategyIsChild_aave test_yield_child_deposit_strategyIsChild_compound</p><p>Encountered 2 failing tests in <strong>test/unit/deposit/ParentDeposit.t.sol:ParentDepositTest</strong> test_yield_parent_deposit_strategyIsChild_aave test_yield_parent_deposit_strategyIsChild_compound</p><p>Encountered 4 failing tests in test/unit/rebalance/Rebalance.t.sol:RebalanceTest test_yield_rebalance_oldChild_newChainC test_yield_rebalance_oldChild_newChild test_yield_rebalance_oldChild_newParent test_yield_rebalance_oldParent_newChild</p><p>Encountered 6 failing tests in <strong>test/unit/withdraw/ChildWithdraw.t.sol:ChildWithdrawTest</strong> test_yield_child_onTokenTransfer_revertsWhen_withdrawChainNotAllowed test_yield_child_withdraw_strategyIsChainC_aave test_yield_child_withdraw_strategyIsChainC_compound test_yield_child_withdraw_strategyIsChild_aave test_yield_child_withdraw_strategyIsChild_aave_withdrawToDifferentChain test_yield_child_withdraw_strategyIsChild_compound</p><p>Encountered 2 failing tests in <strong>test/unit/withdraw/ParentWithdraw.t.sol:ParentWithdrawTest</strong> test_yield_parent_withdraw_strategyIsNotParent_aave test_yield_parent_withdraw_strategyIsNotParent_compound</p><p>we can see the common theme between these tests is the parent ones have a remote strategy, child deposit is remote strategy, and all child withdraw is not parent strategy and rebalance is obvious</p><p>lets start with the rebalance tests.</p><p>4 tests in and i made a new git branch for first time in this project so i dont get all messed up.</p><p>im tired i should finish all of this tomorrow</p><p>currently on main, switch back</p><p>well the good news is the 4 rebalance tests i refactored last night appeared to have solved the issues with the other tests too. very nice. that’s most of todays planned work already done and its only been 5 mins</p><p>[FAIL: ParentRebalancer__OnlyForwarder()] test_yield_performUpkeep_rebalanceNewStrategy() (gas: 313628) [FAIL: ParentRebalancer__OnlyForwarder()] test_yield_performUpkeep_rebalanceOldStrategy() (gas: 314992)</p><p>next thing to do is refactor the invariant handler to account for the rebalancer</p><p>will need to record logs and trigger perform upkeep based on StrategyUpdated(uint64,uint8,uint64)or whatever the event is</p><p>alright whats happening here</p><p>MockCCIPRouter::ccipSend(1, EVM2AnyMessage({ receiver: 0x000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a, data: 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000036f90000000000000000000000000000000000000000000000000000000085572ffa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002, tokenAmounts: [EVMTokenAmount({ token: 0x40b4863C923385D1632d640B097DDeA6EaB92e76, amount: 2237083642 [2.237e9] })], feeToken: 0xc37700132630A8667b38d340d438fce88a8EE7ac, extraArgs: 0x181dcf1000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000000000000001 })) │ │ ├─ [7604] LinkToken::transferFrom(ChildPeer: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], MockCCIPRouter: [0xde930CFb7f7d3b0daEA517CACf0e9ba25740EDe3], 0) │ │ │ ├─ emit Approval(owner: ChildPeer: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], spender: MockCCIPRouter: [0xde930CFb7f7d3b0daEA517CACf0e9ba25740EDe3], value: 0) │ │ │ ├─ emit Transfer(from: ChildPeer: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], to: MockCCIPRouter: [0xde930CFb7f7d3b0daEA517CACf0e9ba25740EDe3], value: 0) │ │ │ └─ ← [Return] true │ │ ├─ [25832] MockUsdc::transferFrom(ChildPeer: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], ParentCLF: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 2237083642 [2.237e9]) │ │ │ ├─ emit Transfer(from: ChildPeer: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], to: ParentCLF: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], value: 2237083642 [2.237e9]) │ │ │ ├─ storage changes: │ │ │ │ @ 0xb3024e141922907eb80bf787d622b0c592108908135c35e38e6ebb7d5636f1e4: 0 → 0x0000000000000000000000000000000000000000000000000000000085572ffa │ │ │ │ @ 0xf1157ff065e42696f046943c33a24ea6ea46f75c9e546ff6f6f449afdd65d2e4: 0x0000000000000000000000000000000000000000000000000000000085572ffa → 0 │ │ │ │ @ 0x6aabb5e3159f63e459180f984db14c38dd83524ae5f05f37086f1b3a53e50715: 0x0000000000000000000000000000000000000000000000000000000085572ffa → 0 │ │ │ └─ ← [Return] true │ │ ├─ [413] ParentCLF::supportsInterface(0x01ffc9a7) [staticcall] │ │ │ └─ ← [Return] true │ │ ├─ [413] ParentCLF::supportsInterface(0xffffffff) [staticcall] │ │ │ └─ ← [Return] false │ │ ├─ [396] ParentCLF::supportsInterface(0x85572ffb) [staticcall] │ │ │ └─ ← [Return] true │ │ ├─ [14710] ParentCLF::ccipReceive(Any2EVMMessage({ messageId: 0x2cc206d4306516d763869c8e287e2d231440722a22ae8354ca1f094999da4772, sourceChainSelector: 2, sender: 0x0000000000000000000000005991a2df15a8f6a256d3ec51e99254cd3fb576a9, data: 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000036f90000000000000000000000000000000000000000000000000000000085572ffa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002, destTokenAmounts: [EVMTokenAmount({ token: 0x40b4863C923385D1632d640B097DDeA6EaB92e76, amount: 2237083642 [2.237e9] })] })) │ │ │ ├─ emit CCIPMessageReceived(messageId: 0x2cc206d4306516d763869c8e287e2d231440722a22ae8354ca1f094999da4772, txType: 0, sourceChainSelector: 2) │ │ │ └─ ← [Revert] panic: arithmetic underflow or overflow (0x11)</p><p>i think the 0 tx type is deposit to parent</p><p>yeah</p><p>hm. i think this is caused by subtracting the amount from total value</p><p>lets see if we can just address the root of this issue</p><p>Failing tests: Encountered 1 failing test in <strong>test/invariant/Invariant.t.sol:Invariant</strong> [FAIL: Invariant violated: User should be able to withdraw what they deposited, except for left over dust]</p><p><strong>invariant_stablecoinRedemptionIntegrity</strong>() (runs: 4, calls: 60, reverts: 0)</p><p>Encountered 2 failing tests in <strong>test/unit/deposit/ChildDeposit.t.sol:ChildDepositTest</strong> [FAIL: assertion failed: 1 != 1000000000000000000000] <strong>test_yield_child_deposit_strategyIsChild_aave</strong>() (gas: 1329910) [FAIL: assertion failed: 1 != 1000000000000000000000] <strong>test_yield_child_deposit_strategyIsChild_compound</strong>() (gas: 1182148)</p><p>Encountered 4 failing tests in <strong>test/unit/withdraw/ChildWithdraw.t.sol:ChildWithdrawTest</strong> [FAIL: assertion failed: 1 != 1000000000000000000000] <strong>test_yield_child_onTokenTransfer_revertsWhen_withdrawChainNotAllowed</strong>() (gas: 1330020) [FAIL: ERC20: transfer amount exceeds balance] <strong>test_yield_child_withdraw_strategyIsChild_aave</strong>() (gas: 1469167) [FAIL: ERC20: transfer amount exceeds balance] <strong>test_yield_child_withdraw_strategyIsChild_aave_withdrawToDifferentChain</strong>() (gas: 1469567) [FAIL: ERC20: transfer amount exceeds balance] <strong>test_yield_child_withdraw_strategyIsChild_compound</strong>() (gas: 1330161)</p><p>interesting</p><p>what did we break here</p><p>i think maybe this may fix….?</p><pre>if (totalShares != 0 || (totalShares != 0 &amp;&amp; totalValue != 0)) {<br>            // if (totalShares != 0 || totalValue != 0) {</pre><p>invariant_stablecoinRedemptionIntegrity() (runs: 9, calls: 135, reverts: 0)</p><p>Encountered a total of 1 failing tests, 132 tests succeeded</p><p>interesting.</p><p>alright finished refactoring unit and invariant tests for the rebalancer</p><p>time to refactor certora</p><h3>ToDO: update readme</h3><p>back to certora</p><pre>certoraRun ./certora/conf/child/BaseChild.conf<br>certoraRun ./certora/conf/parent/BaseParent.conf</pre><pre>certoraRun ./certora/conf/parent/Parent.conf<br>certoraRun ./certora/conf/child/Child.conf</pre><pre>certoraRun ./certora/conf/Yield.conf</pre><pre>certoraRun ./certora/conf/parent/ParentCLF.conf --nondet_difficult_funcs</pre><pre>certoraRun ./certora/conf/parent/Parent.conf --rule setStrategy_movesStrategyToNewChain setStrategy_forwardsRebalanceToOldStrategy</pre><pre>certoraRun ./certora/conf/child/Child.conf --rule handleCCIPWithdrawToStrategy_withdrawsFromStrategy</pre><p>i think maybe these can be deleted and replaced with rebalance/r rules</p><pre>// @review refactor<br>rule setStrategy_movesStrategyToNewChain() {<br>    env e;<br>    uint64 chainSelector;<br>    IYieldPeer.Protocol protocol;<br>    IYieldPeer.Strategy oldStrategy = getStrategy();<br>    require oldStrategy.chainSelector != chainSelector &amp;&amp;<br>            oldStrategy.chainSelector == getThisChainSelector();<br>    uint256 totalValue = getTotalValue(e);<br>    require totalValue &gt; 0;<br>    address strategyPool = getStrategyPool();<br>    uint256 compoundBalanceBefore = usdc.balanceOf(getCompound());<br>    uint256 aaveBalanceBefore = usdc.balanceOf(addressesProvider.getPool());<br>    require strategyPool == getCompound() =&gt; compoundBalanceBefore - totalValue &gt;= 0;<br>    require strategyPool == getAave() =&gt; aaveBalanceBefore - totalValue &gt;= 0;<br>    require usdc.balanceOf(currentContract) == 0;<br>    require ghost_strategyUpdated_eventCount == 0;<br>    require ghost_ccipMessageSent_eventCount == 0;<br>    setStrategy(e, chainSelector, protocol);<br>    assert ghost_strategyUpdated_eventCount == 1;<br>    assert ghost_ccipMessageSent_eventCount == 1;<br>    assert ghost_ccipMessageSent_txType_emitted == 8; // RebalanceNewStrategy<br>    assert ghost_ccipMessageSent_bridgeAmount_emitted == totalValue;<br>    assert strategyPool == getCompound() =&gt; usdc.balanceOf(getCompound()) == compoundBalanceBefore - totalValue;<br>    assert strategyPool == getAave() =&gt; usdc.balanceOf(addressesProvider.getPool()) == aaveBalanceBefore - totalValue;<br>}<br>// @review refactor<br>rule setStrategy_forwardsRebalanceToOldStrategy() {<br>    env e;<br>    uint64 chainSelector;<br>    IYieldPeer.Protocol protocol;<br>    IYieldPeer.Strategy oldStrategy = getStrategy();<br>    require oldStrategy.chainSelector != getThisChainSelector() &amp;&amp;<br>            oldStrategy.chainSelector != chainSelector;<br>    require ghost_ccipMessageSent_eventCount == 0;<br>    setStrategy(e, chainSelector, protocol);<br>    assert ghost_ccipMessageSent_eventCount == 1;<br>    assert ghost_ccipMessageSent_txType_emitted == 7; // RebalanceOldStrategy<br>    assert ghost_ccipMessageSent_bridgeAmount_emitted == 0;<br>}</pre><p>doing the child spec</p><p>handleCCIPWithdrawToStrategy_withdrawsFromStrategy</p><p>withdraw amount is 0x2713</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*242LYNPxlOaDr5ThA0GWxg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j32EH-k1cXWtJgc4Ys2TSg.png" /></figure><p>but this says withdraw amount is 0</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bHZzktMKNbAf2-FKFRHp6Q.png" /></figure><p>probably because this is a dummy value</p><p>need to calculate expected withdraw</p><pre>usdcWithdrawAmount =<br>_calculateWithdrawAmount(totalValue, totalShares, shareBurnAmount);</pre><p>nice already had a harness helper function for this</p><p>oh this rule shouldnt be considering the user balance because its just verifying the function withdraws from the strategy we have other rules for the next 2 behaviours</p><p>not sure why this rule wasnt flagged earlier</p><pre>certoraRun ./certora/conf/parent/Parent.conf</pre><p>alright thats done what next</p><p>should push this, merge the branch, and start a new branch for more certora</p><p>actually i’ll just continue on this branch for a bit and verify the rebalancer behaves as intended</p><pre>certoraRun ./certora/conf/parent/Rebalancer.conf</pre><p>ADD THAT TO README!!!111</p><h3>ToDo:</h3><ol><li>rebalancer verify</li><li>@review comments</li><li>natspec</li><li>mutation testing</li><li>readme update (maybe mention frontend code)</li><li>video fix (maybe)</li><li>improve frontend</li></ol><p>i could fix the black bar at the top of the video or improve the frontend or I could do some mutation testing.</p><p>working through things in the codebase flagged for review. this habit was inspired by cyfrin’s security/auditing course</p><pre>// @review this event<br>        // is this event only emitted when a user deposits, and not a rebalance?<br>        // we should have some rebalance events<br>        emit DepositToStrategyCompleted(strategyPool, amount, totalValue);</pre><p>perhaps the withdraw update should only emit the chain selector since we cant get the amount usdc</p><h3>Coverage report</h3><p>HOW is ParentRebalancer not being hit??????</p><p>oh its because cannotExecute.</p><p>Uncovered for src/modules/ParentRebalancer.sol:</p><ul><li>Line (location: source ID 162, lines 57..58, bytes 3085..3159, hits: 0)</li><li>Statement (location: source ID 162, lines 57..58, bytes 3085..3159, hits: 0)</li><li>Statement (location: source ID 162, lines 57..58, bytes 3110..3159, hits: 0)</li><li>Line (location: source ID 162, lines 58..59, bytes 3169..3202, hits: 0)</li><li>Statement (location: source ID 162, lines 58..59, bytes 3169..3202, hits: 0)</li><li>Line (location: source ID 162, lines 59..60, bytes 3212..3285, hits: 0)</li><li>Statement (location: source ID 162, lines 59..60, bytes 3212..3285, hits: 0)</li><li>Statement (location: source ID 162, lines 59..60, bytes 3239..3285, hits: 0)</li><li>Line (location: source ID 162, lines 60..61, bytes 3295..3326, hits: 0)</li><li>Statement (location: source ID 162, lines 60..61, bytes 3295..3326, hits: 0)</li><li>Line (location: source ID 162, lines 62..63, bytes 3341..3400, hits: 0)</li><li>Statement (location: source ID 162, lines 62..63, bytes 3341..3400, hits: 0)</li><li>Statement (location: source ID 162, lines 62..63, bytes 3341..3365, hits: 0)</li><li>Statement (location: source ID 162, lines 62..63, bytes 3369..3400, hits: 0)</li><li>Branch (branch: 0, path: 0) (location: source ID 162, lines 62..90, bytes 3402..4687, hits: 0)</li><li>Branch (branch: 0, path: 1) (location: source ID 162, lines 62..91, bytes 3337..4703, hits: 0)</li><li>Line (location: source ID 162, lines 63..64, bytes 3416..3469, hits: 0)</li><li>Statement (location: source ID 162, lines 63..64, bytes 3416..3469, hits: 0)</li><li>Line (location: source ID 162, lines 64..65, bytes 3483..3533, hits: 0)</li><li>Statement (location: source ID 162, lines 64..65, bytes 3483..3533, hits: 0)</li><li>Line (location: source ID 162, lines 65..66, bytes 3547..3603, hits: 0)</li><li>Statement (location: source ID 162, lines 65..66, bytes 3547..3603, hits: 0)</li><li>Line (location: source ID 162, lines 67..68, bytes 3622..3697, hits: 0)</li><li>Statement (location: source ID 162, lines 67..68, bytes 3622..3697, hits: 0)</li><li>Statement (location: source ID 162, lines 67..68, bytes 3622..3656, hits: 0)</li><li>Statement (location: source ID 162, lines 67..68, bytes 3660..3697, hits: 0)</li><li>Branch (branch: 1, path: 0) (location: source ID 162, lines 67..72, bytes 3699..3838, hits: 0)</li><li>Line (location: source ID 162, lines 68..69, bytes 3717..3733, hits: 0)</li><li>Statement (location: source ID 162, lines 68..69, bytes 3717..3733, hits: 0)</li><li>Line (location: source ID 162, lines 69..70, bytes 3751..3771, hits: 0)</li><li>Statement (location: source ID 162, lines 69..70, bytes 3751..3771, hits: 0)</li><li>Line (location: source ID 162, lines 70..71, bytes 3789..3823, hits: 0)</li><li>Statement (location: source ID 162, lines 70..71, bytes 3789..3823, hits: 0)</li><li>Line (location: source ID 162, lines 73..75, bytes 3852..4005, hits: 0)</li><li>Statement (location: source ID 162, lines 73..75, bytes 3852..4005, hits: 0)</li><li>Line (location: source ID 162, lines 75..76, bytes 4019..4047, hits: 0)</li><li>Statement (location: source ID 162, lines 75..76, bytes 4019..4047, hits: 0)</li><li>Line (location: source ID 162, lines 76..77, bytes 4061..4127, hits: 0)</li><li>Statement (location: source ID 162, lines 76..77, bytes 4061..4127, hits: 0)</li><li>Statement (location: source ID 162, lines 76..77, bytes 4087..4127, hits: 0)</li><li>Line (location: source ID 162, lines 77..78, bytes 4141..4159, hits: 0)</li><li>Statement (location: source ID 162, lines 77..78, bytes 4141..4159, hits: 0)</li><li>Line (location: source ID 162, lines 79..80, bytes 4178..4253, hits: 0)</li><li>Statement (location: source ID 162, lines 79..80, bytes 4178..4253, hits: 0)</li><li>Statement (location: source ID 162, lines 79..80, bytes 4178..4215, hits: 0)</li><li>Statement (location: source ID 162, lines 79..80, bytes 4219..4253, hits: 0)</li><li>Branch (branch: 2, path: 0) (location: source ID 162, lines 79..83, bytes 4255..4408, hits: 0)</li><li>Branch (branch: 2, path: 1) (location: source ID 162, lines 79..84, bytes 4174..4417, hits: 0)</li><li>Line (location: source ID 162, lines 80..81, bytes 4273..4324, hits: 0)</li><li>Statement (location: source ID 162, lines 80..81, bytes 4273..4324, hits: 0)</li><li>Line (location: source ID 162, lines 81..82, bytes 4342..4393, hits: 0)</li><li>Statement (location: source ID 162, lines 81..82, bytes 4342..4393, hits: 0)</li><li>Line (location: source ID 162, lines 83..84, bytes 4432..4483, hits: 0)</li><li>Statement (location: source ID 162, lines 83..84, bytes 4432..4483, hits: 0)</li><li>Line (location: source ID 162, lines 86..88, bytes 4512..4643, hits: 0)</li><li>Statement (location: source ID 162, lines 86..88, bytes 4512..4643, hits: 0)</li><li>Line (location: source ID 162, lines 88..89, bytes 4657..4676, hits: 0)</li><li>Statement (location: source ID 162, lines 88..89, bytes 4657..4676, hits: 0)</li><li>Line (location: source ID 162, lines 90..91, bytes 4707..4723, hits: 0)</li><li>Statement (location: source ID 162, lines 90..91, bytes 4707..4723, hits: 0)</li><li>Line (location: source ID 162, lines 91..92, bytes 4737..4757, hits: 0)</li><li>Statement (location: source ID 162, lines 91..92, bytes 4737..4757, hits: 0)</li></ul><p>Uncovered for src/peers/ChildPeer.sol:</p><p>Uncovered for src/peers/ParentPeer.sol:</p><ul><li>Branch (branch: 25, path: 0) (location: source ID 165, lines 444..445, bytes 26066..26085, hits: 0)</li><li>Statement (location: source ID 165, lines 444..445, bytes 26066..26085, hits: 0)</li></ul><p>Uncovered for src/peers/YieldPeer.sol:</p><ul><li>Branch (branch: 11, path: 0) (location: source ID 166, lines 444..445, bytes 23661..23693, hits: 0)</li><li>Statement (location: source ID 166, lines 444..445, bytes 23661..23693, hits: 0)</li></ul><p>the parentPeer unhit line is</p><p>if (shareMintAmount == 0) shareMintAmount = 1; which is covered in certora</p><p>YieldPeer:</p><pre>/// @dev Revert if the amount is 0<br>    /// @param amount The amount to check<br>    function _revertIfZeroAmount(uint256 amount) internal pure {<br>        if (amount == 0) revert YieldPeer__NoZeroAmount();<br>    }</pre><p>this is only used for checking onTokenTransfer isnt 0, but im not sure if that isnt already handled with the actual transferAndCall. will have to double check.</p><p>RUN ALL CERTORA CONFS AGAIN AFTER REVIEWS</p><p>come back to DepositData.amount and WithdrawData.usdcWithdrawAmount after hackathon</p><p>the problem with emitting withdraw update on parent in an attempt to track total withdraws is that there will be scenarios when the withdraw amount is never passed through the parent</p><p>in that case maybe i should just get rid of it</p><p>i think i can get rid of the deposit and withdraw updates. they are adding clutter and not really helping to test</p><p>reviews are done and certora is passing.</p><p>can probably start mutation testing although i know the certora specs are missing some event param verification</p><p>set crosschain vars script is not tested as well as i would like</p><p>[obscenities] this script test</p><p>i know the script works perfectly. i’ve used it repeatedly on testnets. but i cant test it to get to 100% unit coverage because pranking and broadcast are incompatible. i cant prank the owner of the contracts before broadcast to transfer ownership to the test because the networkconfig changes the owner for broadcasts. [obscenities]</p><p>this is not happening</p><p>theres no way to do it</p><p>i only want it because in coverage it shows 0%, standing out in my sea of 100%. how likely is it that any judge is going to git clone, forge install, forge coverage and also care?</p><p>i care.</p><p>cant fork mainnets and deploy there because the network config is cached in the script. reassigning it in test doesnt work.</p><p>[FAIL: assertion failed: 2000000000000000000000 != 1999999999500000000249] test_yield_parent_deposit_multipleDeposits()</p><h3>Another list</h3><ol><li>reupload video</li><li>frontend header</li><li>get rid of artifact</li><li>add frontend to repo</li><li>improve frontend (?) display and balance loading</li><li>redeploy bug free infra on testnets?</li><li>write article on bug</li></ol><h3>post hackathon idea</h3><p>consider doing articles on findings ie the total value — amount thing, why it was happening</p><p>the incorrect order of operations</p><ul><li>depositing</li><li>then getting value</li></ul><p>what the affect was</p><p>this might sound like a simple mistake, but this incorrect order of operations buried deep in an abstract contract’s helper function was the cause of a critical invariant breaking during this hackathon. maths is the most important thing.</p><h3>final checklist</h3><ul><li>run all tests</li><li>run all certora confs</li><li>update readme</li><li>update submission</li><li>check @review</li></ul><p>wew we are close to a polished submission. let’s do some certora</p><p>! v63 of the <a href="http://v0.dev">v0.dev</a> is what is deployed ! (dependencies edited in repo)</p><p>i could redeploy the entire infra…….. how much of a difference to the submission would that make…… lets just stick with more certora for now.</p><p>or i could write my article</p><h3>Chromion hackathon: YieldCoin mint calculation bug</h3><p><em>YieldCoin is an automated crosschain stablecoin yield optimizer, built for the Chainlink Chromion hackathon. In this article we will be discussing the most significant part of the development, a critical invariant breaking bug.</em></p><h3>Context</h3><p>The YieldCoin architecture allows users to deposit stablecoins from their chain of choice, to earn the highest yield available. YieldCoin smart contracts use Chainlink to find the highest APY across chains and protocols, then rebalance the funds to where that is.</p><p>In exchange for depositing USDC, users receive YieldCoin, a token representing the user’s share of the YieldCoin TVL. The basic usecase of YieldCoin offered to stablecoin holders is that they can deposit their money, earn the maximum amount over time on it without any input or risk, then withdraw their deposit and earned yield at their leisure.</p><h3>The Invariant</h3><p>A key invariant of this sytem, perhaps <em>the</em> key invariant, is users should always be able to withdraw the stablecoin amount they deposited (minus any fees, or immediate short term wei differences). If this invariant breaks then users can’t withdraw their money. So it’s important to not mess it up.</p><h3>Bug Discovery</h3><p>Once full unit coverage was achieved for the YieldCoin codebase, invariant tests in Foundry were started. Testing other invariants such as the integrity of the total crosschain supply of YieldCoin and YieldCoin TVL inadvertently exposed cracks in the YieldCoin minting logic.</p><pre>/// @notice Total Shares Accountancy: The total shares tracked by ParentPeer should be equal to total minted minus total burned system wide.<br>    function invariant_totalShares_integrity() public view {<br>        assertEq(<br>            parent.getTotalShares(),<br>            handler.ghost_event_totalSharesMinted() - handler.ghost_state_totalSharesBurned(),<br>            &quot;Invariant violated: Total shares tracked by ParentPeer should be equal to total minted minus total burned system wide.&quot;<br>        );<br>    }<br><br>/// @notice Total Value Accountancy: The total value in the system should be more than or equal to total USDC deposited minus total USDC withdrawn<br>    function invariant_totalValue_integrity() public {<br>        handler.forEachChainSelector(this.checkTotalDepositsAgainstTotalValuePerChainSelector);<br>    }<br>    function checkTotalDepositsAgainstTotalValuePerChainSelector(uint64 chainSelector) external view {<br>        uint256 totalDeposited = handler.ghost_state_totalUsdcDeposited();<br>        uint256 totalWithdrawn = handler.ghost_event_totalUsdcWithdrawn();<br>        uint256 netDeposits = totalDeposited &gt; totalWithdrawn ? totalDeposited - totalWithdrawn : 0;<br>        if (chainSelector == parent.getStrategy().chainSelector) {<br>            assertTrue(<br>                IYieldPeer(handler.chainSelectorsToPeers(chainSelector)).getTotalValue() &gt;= netDeposits,<br>                &quot;Invariant violated: Total value in the system should be more than or equal to total USDC deposited minus total USDC withdrawn&quot;<br>            );<br>        }<br>    }</pre><p>(These may not have been the exact implementations at the time).</p><p>The issue was initially chalked down to the same inflation attack experienced by ERC4626 vaults, and the issue remained hidden during the rest of this invariant testing phase.</p><h3>Bug Reemergence</h3><p>The bug resurfaced when writing unit test style rules with Certora.</p><pre>rule deposit_mintsShares_when_parent_is_strategy() {<br>    env e;<br>    calldataarg args;<br>    require getStrategy().chainSelector == getThisChainSelector();<br>    <br>    uint256 shareSupplyBefore = share.totalSupply();<br>    uint256 totalSharesBefore = getTotalShares();<br><br>    require inflationAttackMitigation();<br>    deposit(e, args);<br>    assert share.totalSupply() &gt; shareSupplyBefore;<br>    assert getTotalShares() &gt; totalSharesBefore;<br>}</pre><p>This simple rule, that merely expects the YieldCoin total supply to increase whenever stablecoins are deposited, was failing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*LiAlSR56_mmkAmjncu3qKw.jpeg" /></figure><p>Certora was revealing cases where addresses were depositing amounts of USDC and not receiving any YieldCoin minted in exchange. This confirmed the YieldCoin minting calculation bug had not been mitigated as previously assumed and a critical invariant was broken.</p><h3>Back to Foundry Invariants</h3><p>Given the way the Certora Prover explores paths and havocs variables, verifying crosschain invariants like YieldCoin’s integrity of deposit redemption can be tricky. Why? Maybe it could be done by linking a Mock CCIP router in the Certora scene and requiring certain behaviours. This needs to be revisited because it might be possible.</p><p>Anyway the invariant needed to be clearly defined and doing it in Foundry was ironically quicker because the crosschain infrastructure had already been deployed locally, so no time needed to be spent on that idea I just came up with in the last paragraph.</p><h3>Defining the invariant</h3><p>Getting the precise definition of the invariant required tweaking and acknowledgement of limitations and scenarios. There was already a known issue where burning insignificantly small amounts of YieldCoin (worth less than the lowest denomination of USDC, 6 decimals) redeemed 0 USDC. So accounting for that had to be taken into consideration.</p><p>1% slippage tolerance was also applied, although maybe not entirely necessary.</p><pre>/// @notice Users should always be able to withdraw what they deposited (minus fees, but those arent implemented yet)<br>    /// @dev this is a critical invariant that ensures the integrity of user deposit redemption<br>    function invariant_stablecoinRedemptionIntegrity() public {<br>        handler.forEachUser(this.checkRedemptionIntegrityPerUser);<br>    }<br><br>function checkRedemptionIntegrityPerUser(address user) external view {<br>        uint256 deposited = handler.ghost_state_totalUsdcDepositedPerUser(user);<br>        uint256 withdrawn = handler.ghost_event_totalUsdcWithdrawnPerUser(user);<br>        uint256 netDeposits = deposited &gt; withdrawn ? deposited - withdrawn : 0;<br>        uint256 userShares = share.balanceOf(user);<br>        uint256 totalValue =<br>            IYieldPeer(handler.chainSelectorsToPeers(parent.getStrategy().chainSelector)).getTotalValue();<br>        uint256 totalValueConverted = _convertUsdcToShare(totalValue);<br>        uint256 minUsdcValueInShares = _convertUsdcToShare(1);<br>        uint256 totalShares = parent.getTotalShares();<br>        if (totalShares &gt; 0) {<br>            uint256 withdrawable = (userShares * totalValueConverted) / totalShares;<br>            uint256 withdrawableConverted = _convertShareToUsdc(withdrawable);<br>            uint256 minWithdrawable = netDeposits * 990 / 1000; // Allow 1% slippage<br>            if (withdrawableConverted &lt; minWithdrawable) {}<br>            assertTrue(<br>                withdrawableConverted &gt;= minWithdrawable || netDeposits &lt; minUsdcValueInShares,<br>                &quot;Invariant violated: User should be able to withdraw what they deposited, except for left over dust&quot;<br>            );<br>        } else {<br>            assertTrue(netDeposits == 0, &quot;Invariant violated: User should be able to withdraw what they deposited&quot;);<br>        }<br>    }</pre><p>This invariant asserts that any YieldCoin holder can always withdraw 99% of the USDC amount they deposited, <em>or</em> the amount of YieldCoin dust they hold is worth less than the lowest denomination of USDC.</p><p>And it was breaking, but in doing so, led us to where in the codebase the issue was happening.</p><p>Adding console logs in the invariant gave greater insight into what was happening.</p><pre>console2.log(&quot;User:&quot;, user);<br>console2.log(&quot;Deposited:&quot;, deposited);<br>console2.log(&quot;Withdrawn:&quot;, withdrawn);<br>console2.log(&quot;Net Deposits:&quot;, netDeposits);<br>console2.log(&quot;User Shares:&quot;, userShares);<br>console2.log(&quot;Total Value:&quot;, totalValue);<br>console2.log(&quot;Total Shares:&quot;, totalShares);<br>console2.log(&quot;Withdrawable:&quot;, withdrawable);<br>console2.log(&quot;Min Withdrawable:&quot;, minWithdrawable);</pre><p>This made it easier to see the discrepancies between how much USDC a user could withdraw and how much they should’ve been able to withdraw.</p><h3>Unit test checks</h3><p>To see what was happening as simply as possible, I wrote a unit test where 3 users deposited the same amount of USDC into the contracts, one after the other. In theory they should have received (roughly) the same amount of YieldCoin in exchange for their USDC deposits because their deposits were equal.</p><p>These were the results:</p><p>emit SharesMinted(to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 1000000000000000000000 [1e21])</p><p>emit SharesMinted(to: user2: [0x537C8f3d3E18dF5517a58B3fB9D9143697996802], amount: 500000000000000000000 [5e20])</p><p>emit SharesMinted(to: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 499999999833333333388 [4.999e20])</p><p>Now the problem was very clear.</p><h3>Breaking the invariant</h3><p>So what was happening?</p><p>In most cases when a user deposited USDC into the YieldCoin contracts, the following function would be called to:</p><ol><li>deposit their USDC to the yield generating strategy</li><li>return the totalValue of the system to calculate their YieldCoin mint amount</li></ol><pre>/// @notice Deposits USDC to the strategy and returns the total value of the system<br>/// @param amount The amount of USDC to deposit<br>/// @return totalValue The total value of the system<br>function _depositToStrategyAndGetTotalValue(uint256 amount) internal returns (uint256 totalValue) {<br>    address strategyPool = _getStrategyPool();<br>    _depositToStrategy(strategyPool, amount);<br>    totalValue = _getTotalValueFromStrategy(strategyPool);<br>    emit DepositCompleted(strategyPool, amount, totalValue);<br>}</pre><p>The totalValue, or “YieldCoin TVL”, is all of the deposits + generated yield in the system. This value is used alongside the user’s usdcDepositAmount and the totalShares, or YieldCoin total crosschain supply, to calculate the shareMintAmount - the amount of YieldCoin to mint for a user.</p><p>The incorrect order of these lines meant that the usdcDepositAmount was being included with the totalValue because the _depositToStrategy() call was happening before the _getTotalValueFromStrategy() call.</p><pre>_depositToStrategy(strategyPool, amount);<br>totalValue = _getTotalValueFromStrategy(strategyPool);</pre><p>This means the totalValue being returned by this function, used to calculate the amount of YieldCoin to mint to a depositor, was more than it should’ve been, resulting in a lower YieldCoin mint amount, losing user funds - very bad!</p><h3>Fixing the invariant</h3><p>The obvious (and correct) solution was to swap the lines around.</p><pre>totalValue = _getTotalValueFromStrategy(strategyPool);<br>_depositToStrategy(strategyPool, amount);</pre><p>Even though this had been done, the invariant continued breaking.</p><p>This is the line that performs the calculation:</p><pre>shareMintAmount = <br>	(_convertUsdcToShare(usdcDepositAmount) * totalShares) <br>		/ _convertUsdcToShare(totalValue);</pre><p>The immediate fix was to subtract usdcDepositAmount from the totalValue like so:</p><pre>shareMintAmount = <br>	(_convertUsdcToShare(usdcDepositAmount) * totalShares) <br>		/ _convertUsdcToShare(totalValue - usdcDepositAmount);</pre><p>Not a particularly efficient or aesthetic fix.</p><p>Digging through the contracts revealed a single instance of the incorrect deposit and total value fetch operations also happening outside the helper function used everywhere else.</p><p>These repetitive lines of code were replaced with a call to the _depositToStrategyAndGetTotalValue() function, with the correct order of operations. The invariant was now fixed and the usdcDepositAmount no longer needed to be subtracted from the totalValue in the calculation.</p><h3>Conclusion</h3><p>The key takeaways from this are the importance of:</p><ul><li>using multiple tools for fuzzing and verification</li><li>full unit coverage is only the beginning of testing</li><li>maths</li><li><a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a> code</li></ul><p><em>YieldCoin was built for the Chainlink Chromion hackathon 2025 — see the project </em><a href="https://devfolio.co/projects/yieldcoin-112a"><em>here</em></a><em>.</em></p><p>Yieldcoin’s certora specs are fragmented due to its crosschain architecture, but all bugs introduced by the first certoraMutate are caught between two of them! — double check this</p><p>the 2nd certoraMutate run is revealing 3 pathways that are not covered by our specs.</p><h3>1. ChildPeer::deposit:</h3><pre>function deposit(uint256 amountToDeposit) external override {<br>        _initiateDeposit(amountToDeposit);<br>        address strategyPool = _getStrategyPool();<br>        DepositData memory depositData = _buildDepositData(amountToDeposit);<br>        // 1. This Child is the Strategy<br>        /// IfStatementMutation(`strategyPool != address(0)` |==&gt; `true`) of: `if (strategyPool != address(0)) {`<br>        if (true) {<br>            /// @dev deposit USDC in strategy pool and get totalValue<br>            depositData.totalValue = _depositToStrategyAndGetTotalValue(amountToDeposit);<br>            /// @dev send a message to parent contract to request shareMintAmount<br>            _ccipSend(<br>                i_parentChainSelector, CcipTxType.DepositCallbackParent, abi.encode(depositData), ZERO_BRIDGE_AMOUNT<br>            );<br>        }<br>        // 2. This Child is not the Strategy<br>        else {<br>            /// @dev send a message to parent contract to deposit to strategy and request shareMintAmount<br>            _ccipSend(i_parentChainSelector, CcipTxType.DepositToParent, abi.encode(depositData), amountToDeposit);<br>        }<br>    }</pre><h3>2. ChildPeer::deposit</h3><pre>function deposit(uint256 amountToDeposit) external override {<br>        _initiateDeposit(amountToDeposit);<br>        address strategyPool = _getStrategyPool();<br>        DepositData memory depositData = _buildDepositData(amountToDeposit);<br>        // 1. This Child is the Strategy<br>        if (strategyPool != address(0)) {<br>            /// @dev deposit USDC in strategy pool and get totalValue<br>            /// AssignmentMutation(`_depositToStrategyAndGetTotalValue(amountToDeposit)` |==&gt; `1`) of: `depositData.totalValue = _depositToStrategyAndGetTotalValue(amountToDeposit);`<br>            depositData.totalValue = 1;<br>            /// @dev send a message to parent contract to request shareMintAmount<br>            _ccipSend(<br>                i_parentChainSelector, CcipTxType.DepositCallbackParent, abi.encode(depositData), ZERO_BRIDGE_AMOUNT<br>            );<br>        }<br>        // 2. This Child is not the Strategy<br>        else {<br>            /// @dev send a message to parent contract to deposit to strategy and request shareMintAmount<br>            _ccipSend(i_parentChainSelector, CcipTxType.DepositToParent, abi.encode(depositData), amountToDeposit);<br>        }<br>    }</pre><h3>3. ChildPeer::_handleCCIPMessage</h3><pre>function _handleCCIPMessage(<br>        CcipTxType txType,<br>        Client.EVMTokenAmount[] memory tokenAmounts,<br>        bytes memory data,<br>        uint64 /* sourceChainSelector */<br>    ) internal override {<br>        if (txType == CcipTxType.DepositToStrategy) _handleCCIPDepositToStrategy(tokenAmounts, data);<br>        if (txType == CcipTxType.DepositCallbackChild) _handleCCIPDepositCallbackChild(data);<br>        if (txType == CcipTxType.WithdrawToStrategy) _handleCCIPWithdrawToStrategy(data);<br>        /// DeleteExpressionMutation(`_handleCCIPWithdrawCallback(tokenAmounts, data)` |==&gt; `assert(true)`) of: `if (txType == CcipTxType.WithdrawCallback) _handleCCIPWithdrawCallback(tokenAmounts, data);`<br>        if (txType == CcipTxType.WithdrawCallback) assert(true);<br>        if (txType == CcipTxType.RebalanceOldStrategy) _handleCCIPRebalanceOldStrategy(data);<br>        if (txType == CcipTxType.RebalanceNewStrategy) _handleCCIPRebalanceNewStrategy(data);<br>    }</pre><p>there is a DepositToStrategy event and a DepositToStrategyCompleted event</p><p>sometimes 1 of these events is emitted. sometimes both.</p><pre>/// @notice Internal helper to deposit to the strategy<br>    /// @param strategyPool The strategy pool to deposit to<br>    /// @param amount The amount of USDC to deposit<br>    /// @dev Emit DepositToStrategy event<br>    function _depositToStrategy(address strategyPool, uint256 amount) internal {<br>        ProtocolOperations._depositToStrategy(strategyPool, _getProtocolConfig(), amount);<br>        emit DepositToStrategy(strategyPool, amount);<br>    }</pre><pre>/// @notice Deposits USDC to the strategy and returns the total value of the system<br>    /// @param amount The amount of USDC to deposit<br>    /// @return totalValue The total value of the system // _getTotalValueAndDepositToStrategy<br>    function _depositToStrategyAndGetTotalValue(uint256 amount) internal returns (uint256 totalValue) {<br>        address strategyPool = _getStrategyPool();<br>        totalValue = _getTotalValueFromStrategy(strategyPool);<br>        _depositToStrategy(strategyPool, amount);<br>        emit DepositToStrategyCompleted(strategyPool, amount, totalValue);<br>    }</pre><p>this is confusing and a clear area for improvement</p><h3>Penultimate day</h3><ol><li>REVIEW ALL LINKS IN README</li><li>REVIEW SUBMISSION</li><li>RUN ALL TESTS AND CERTORA</li></ol><p>with certora rules, you’re verifying behaviour down a particular pathway, whereas with invariants you’re verifying it down all the pathways</p><p>could use chainlink price feeds to calculate minimum share burnt amount but difficult due to the crosschain nature of the usdcWithdrawAmount calculation</p><p>all great businesses and innovations of the future will be built with chainlink. there is no better opportunity than spending one’s time on chainlink services, smart contract security</p><p>of all the other projects i’ve seen so far this has the best tests: <a href="https://devfolio.co/projects/emerald-dao-d924">https://devfolio.co/projects/emerald-dao-d924</a></p><h3>Reflections</h3><p>after getting an eligible submission complete, progress on the project could’ve gone 1 of 2 ways: additional features or additional testing. The choice was made to prioritize security of the existing protocol before considering new features. Security is the most important thing when it comes to smart contract development. Nobody wants to use a contract that will break and lose their money.</p><p>The bare minimum for each feature should be something akin to this list <a href="https://x.com/contractlevel/status/1928756157088202812">(cyfrin tweet)</a></p><p>Plus once you have the extensive testing infrastructure, you reuse it for every feature addition, to see if anything is now breaking.</p><p>An ideal team would have 50% of the team always extending the test suite, ie once invariants are done in foundry, move on to certora, halmos, chimera, etc. The aim should be for the codebase to be as covered/layered with testing infrastructure as possible. A smart contract codebase is far from finished if the behavior of any pathways have not been verified.</p><blockquote>if the attack vector exists, it will be exploited</blockquote><p>getting good at writing/securing smart contracts will be the best way to have a lasting impact</p><p>With the way AI is going, it’s easy to see how this process of extensive testing for every feature will get quicker. But you still have to know what you are doing. Anyone can know what they are doing, as long as they put in consistent time.</p><p>The testing infrastructure for YieldCoin’s current state is still incomplete because there are pathways that haven’t been explored. More time must be spent considering states.</p><p>The DepositToStrategyCompleted and DepositToStrategy events must be reviewed further due to their confusing uses and names. I will do that now.</p><p>oh right DepositToStrategyCompleted is used for when a user deposits, but DepositToStrategy is emitted when both a user deposits and the system rebalances tvl by depositing. that’s confusing.</p><p>we probably need rebalance events</p><p>to make it clear when a rebalance deposit completes</p><p>When it comes to verifying paths in certora, you can be as granular as you like. Harnesses can expose everything so the path trace can begin and end where you like</p><p>mutating the parent rebalancer has confirmed my thesis that reverting instead of returning false for chainlink automation is better for security because valid paths can be more confidently verified.</p><p>i should really change it. i’d have to refactor unit tests, invariant tests will <em>probably</em> not need refactoring. certora specs would need refactoring too</p><p>If a user is providing input to your system then you need to verify all the possible pathways result in valid states</p><p>ok im replacing the return false with reverts</p><p>do we need implication operator to pass the local rebalance revert check</p><p>require newSelector ≠ parentSelector ⇒ oldSelector ≠ parentSelector;</p><p>and vice versa? maybe?</p><p>or something like</p><p>require newSelector == parent ⇒ old ≠ parent?</p><p>require old == parent ⇒ new ≠ parent</p><p>Why did this call revert?</p><p><strong>REVERT CAUSE</strong></p><p><em>keyboard_arrow_down</em></p><p>See \”log.topics[1]\” @ src/modules/ParentRebalancer.sol: line 64</p><p><strong>CANON95&lt;=0x1</strong></p><p>what does this mean</p><p>i am tired and this hackathon has 24 hours left</p><h3>Final day</h3><p>ok today is the final day. i hope it doesnt get extended, dont think it will</p><p>just published the article on the bug, now lets see if we can finish this certora spec for the new checkLog</p><p>mfs in discord asking for extension. bruh i just put in like 12 hours a day for the last month. gtfo</p><p>so we are requiring the topics inside this cvl func</p><pre>require topics[0] == eventSignature;<br>    require topics[1] == uint64ToBytes32(newChainSelector);<br>    require topics[2] == uint8ToBytes32(protocolEnum);<br>    require topics[3] == uint64ToBytes32(oldChainSelector);</pre><p>yet these bits of the certora calltrace:</p><pre>Why did this call revert?</pre><pre>REVERT CAUSE<br>See \\&quot;log.topics[1]\\&quot; @ src/modules/ParentRebalancer.sol: line 64</pre><pre>CANON95&lt;=0x1</pre><pre>RebalancerHarness.checkLog(log={index=0x197d, timestamp=0xbc1, txHash=0x0, blockNumber=0x9a9, blockHash=0x0, source=ParentPeer,<br>topics=[0=0xcb3...ac7] (length=1),<br>data=bytes (length=0x0)}, param_1=bytes (length=0x0)) ↪ ..., ...</pre><p>topics=[0=0xcb3…ac7] (length=1)</p><p>is implying that the other topics are not there</p><p>the event sig looks correct so theres that</p><pre>definition StrategyUpdatedEvent() returns bytes32 =<br>// keccak256(abi.encodePacked(&quot;StrategyUpdated(uint64,uint8,uint64)&quot;))<br>    to_bytes32(0xcb31617872c52547b670aaf6e63c8f6be35dc74d4144db1b17f2e539b5475ac7);</pre><p>i fixed that by requiring the topics.length == 4 (to include each emitted param)</p><p>that made the rules vacuous, so no difference between returning false and reverting when upkeep not needed.</p><p>i’ll probably just run all the tests again, leave a note acknowledging this in the readme, merge the pr and that’ll be it for this hackathon entry.</p><p>final coverage:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uThZUPM72A9wiTH9cswnlw.png" /></figure><p>all invariants pass with 3000 runs and certora rules pass too (except vacuous ones with sanity)</p><p>unit coverage doesnt include set crosschain vars script because i spent ages trying to make it work without adjusting the broadcast and pranking but it wasnt worth it. already works perfectly on testnets.</p><p>there’s 14 hours left to go. i dont think there’s much more to do. the only thing that arguably needs improving is changing the name of one of those deposit events but its so minor</p><p>I don’t know if anyone is actually going to read through all of these notes. i’ll read them again in a few years.</p><p>I should run all tests a final time as well as certora specs, and slither and aderyn. hmm aderyn showing unchecked return values for some approve calls…………</p><p>gm to everyone who writes articles about finding and fixing bugs with certora and invariant tests in their chainlink hackathon project</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=26685753741c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Chromion hackathon: YieldCoin mint calculation bug]]></title>
            <link>https://medium.com/@contractlevel/chromion-hackathon-yieldcoin-mint-calculation-bug-d314e9dc623f?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/d314e9dc623f</guid>
            <category><![CDATA[chain-link]]></category>
            <category><![CDATA[smart-contract-security]]></category>
            <category><![CDATA[stable-coin]]></category>
            <category><![CDATA[invariants]]></category>
            <category><![CDATA[certora]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Sun, 29 Jun 2025 10:09:04 GMT</pubDate>
            <atom:updated>2025-07-17T15:13:43.786Z</atom:updated>
            <content:encoded><![CDATA[<p><em>YieldCoin is an automated crosschain stablecoin yield optimizer, built for the Chainlink Chromion hackathon. In this article we will be discussing the most significant part of the development, a critical invariant breaking bug.</em></p><h3>Context</h3><p>The YieldCoin architecture allows users to deposit stablecoins from their chain of choice, to earn the highest yield available. YieldCoin smart contracts use Chainlink to find the highest APY across chains and protocols, then rebalance the funds to where that is.</p><p>In exchange for depositing USDC, users receive YieldCoin, a token representing the user’s share of the YieldCoin TVL. The basic usecase of YieldCoin offered to stablecoin holders is that they can deposit their money, earn the maximum amount over time on it without any input or risk, then withdraw their deposit and earned yield at their leisure.</p><h3>The Invariant</h3><p>A key invariant of this sytem, perhaps <em>the</em> key invariant, is users should always be able to withdraw the stablecoin amount they deposited (minus any fees, or immediate short term wei differences). If this invariant breaks then users can’t withdraw their money. So it’s important to not mess it up.</p><h3>Bug Discovery</h3><p>Once full unit coverage was achieved for the YieldCoin codebase, invariant tests in Foundry were started. Testing other invariants such as the integrity of the total crosschain supply of YieldCoin and YieldCoin TVL inadvertently exposed cracks in the YieldCoin minting logic.</p><pre>/// @notice Total Shares Accountancy: The total shares tracked by ParentPeer should be equal to total minted minus total burned system wide.<br>function invariant_totalShares_integrity() public view {<br>    assertEq(<br>        parent.getTotalShares(),<br>        handler.ghost_event_totalSharesMinted() - handler.ghost_state_totalSharesBurned(),<br>        &quot;Invariant violated: Total shares tracked by ParentPeer should be equal to total minted minus total burned system wide.&quot;<br>    );<br>}<br><br>/// @notice Total Value Accountancy: The total value in the system should be more than or equal to total USDC deposited minus total USDC withdrawn<br>function invariant_totalValue_integrity() public {<br>    handler.forEachChainSelector(this.checkTotalDepositsAgainstTotalValuePerChainSelector);<br>}<br><br>function checkTotalDepositsAgainstTotalValuePerChainSelector(uint64 chainSelector) external view {<br>    uint256 totalDeposited = handler.ghost_state_totalUsdcDeposited();<br>    uint256 totalWithdrawn = handler.ghost_event_totalUsdcWithdrawn();<br>    uint256 netDeposits = totalDeposited &gt; totalWithdrawn ? totalDeposited - totalWithdrawn : 0;<br>    if (chainSelector == parent.getStrategy().chainSelector) {<br>        assertTrue(<br>            IYieldPeer(handler.chainSelectorsToPeers(chainSelector)).getTotalValue() &gt;= netDeposits,<br>            &quot;Invariant violated: Total value in the system should be more than or equal to total USDC deposited minus total USDC withdrawn&quot;<br>        );<br>    }<br>}</pre><p>The issue was initially chalked down to the same inflation attack experienced by ERC4626 vaults, and the issue remained hidden during the rest of this invariant testing phase with an “initial admin donation deposit”.</p><h3>Bug Reemergence</h3><p>The bug resurfaced when writing unit test style rules with Certora.</p><pre>rule deposit_mintsShares_when_parent_is_strategy() {<br>    env e;<br>    calldataarg args;<br><br>    require getStrategy().chainSelector == getThisChainSelector();<br>    <br>    uint256 shareSupplyBefore = share.totalSupply();<br>    uint256 totalSharesBefore = getTotalShares();<br><br>    require inflationAttackMitigation();<br><br>    deposit(e, args);<br><br>    assert share.totalSupply() &gt; shareSupplyBefore;<br>    assert getTotalShares() &gt; totalSharesBefore;<br>}</pre><p>This simple rule, that merely expects the YieldCoin total supply to increase whenever stablecoins are deposited, was failing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*LiAlSR56_mmkAmjncu3qKw.jpeg" /></figure><p>Certora was revealing cases where addresses were depositing amounts of USDC and not receiving any YieldCoin minted in exchange. This confirmed the YieldCoin minting calculation bug had not been mitigated as previously assumed and a critical invariant was broken.</p><h3>Back to Foundry Invariants</h3><p>Given the way the Certora Prover explores paths and havocs variables, verifying crosschain invariants like YieldCoin’s integrity of deposit redemption can be tricky. Why? Maybe it could be done by linking a Mock CCIP router in the Certora scene and requiring certain behaviours. This needs to be revisited because it might be possible.</p><p>Anyway the invariant needed to be clearly defined and doing it in Foundry was ironically quicker because the crosschain infrastructure had already been deployed locally, so no time needed to be spent on that idea I just came up with in the last paragraph.</p><h3>Defining the invariant</h3><p>Getting the precise definition of the invariant required tweaking and acknowledgement of limitations and scenarios. There was already a known issue where burning insignificantly small amounts of YieldCoin (worth less than the lowest denomination of USDC, 6 decimals) redeemed 0 USDC. So accounting for that had to be taken into consideration.</p><p>1% slippage tolerance was also applied.</p><pre>/// @notice Users should always be able to withdraw what they deposited (minus fees, but those arent implemented yet)<br>/// @dev this is a critical invariant that ensures the integrity of user deposit redemption<br>function invariant_stablecoinRedemptionIntegrity() public {<br>    handler.forEachUser(this.checkRedemptionIntegrityPerUser);<br>}<br><br>function checkRedemptionIntegrityPerUser(address user) external view {<br>    uint256 deposited = handler.ghost_state_totalUsdcDepositedPerUser(user);<br>    uint256 withdrawn = handler.ghost_event_totalUsdcWithdrawnPerUser(user);<br>    uint256 netDeposits = deposited &gt; withdrawn ? deposited - withdrawn : 0;<br>    uint256 userShares = share.balanceOf(user);<br><br>    uint256 totalValue =<br>        IYieldPeer(handler.chainSelectorsToPeers(parent.getStrategy().chainSelector)).getTotalValue();<br>    uint256 totalValueConverted = _convertUsdcToShare(totalValue);<br>    uint256 minUsdcValueInShares = _convertUsdcToShare(1);<br>    uint256 totalShares = parent.getTotalShares();<br><br>    if (totalShares &gt; 0) {<br>        uint256 withdrawable = (userShares * totalValueConverted) / totalShares;<br>        uint256 withdrawableConverted = _convertShareToUsdc(withdrawable);<br>        uint256 minWithdrawable = netDeposits * 990 / 1000; // Allow 1% slippage<br>        if (withdrawableConverted &lt; minWithdrawable) {}<br>        assertTrue(<br>            withdrawableConverted &gt;= minWithdrawable || netDeposits &lt; minUsdcValueInShares,<br>            &quot;Invariant violated: User should be able to withdraw what they deposited, except for left over dust&quot;<br>        );<br>    } else {<br>        assertTrue(netDeposits == 0, &quot;Invariant violated: User should be able to withdraw what they deposited&quot;);<br>    }<br>}</pre><p>This invariant asserts that any YieldCoin holder can always withdraw 99% of the USDC amount they deposited, <em>or</em> the amount of YieldCoin dust they hold is worth less than the lowest denomination of USDC.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cpW9ss7CBLw72pOgrgjKbg.jpeg" /></figure><p>And it was breaking, but in doing so, led us to where in the codebase the issue was happening.</p><p>Adding console logs in the invariant gave greater insight into what was happening.</p><pre>console2.log(&quot;User:&quot;, user);<br>console2.log(&quot;Deposited:&quot;, deposited);<br>console2.log(&quot;Withdrawn:&quot;, withdrawn);<br>console2.log(&quot;Net Deposits:&quot;, netDeposits);<br>console2.log(&quot;User Shares:&quot;, userShares);<br>console2.log(&quot;Total Value:&quot;, totalValue);<br>console2.log(&quot;Total Shares:&quot;, totalShares);<br>console2.log(&quot;Withdrawable:&quot;, withdrawable);<br>console2.log(&quot;withdrawableConverted:&quot;, withdrawableConverted);<br>console2.log(&quot;Min Withdrawable:&quot;, minWithdrawable);</pre><p>This made it easier to see the discrepancies between how much USDC a user could withdraw and how much they should’ve been able to withdraw.</p><h3>Unit test checks</h3><p>To see what was happening as simply as possible, I wrote a unit test where 3 users deposited the same amount of USDC into the contracts, one after the other. In theory they should have received (at least roughly) the same amount of YieldCoin in exchange for their USDC deposits because their deposits were equal.</p><p>These were the results:</p><p>emit SharesMinted(to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 1000000000000000000000 [1e21])</p><p>emit SharesMinted(to: user2: [0x537C8f3d3E18dF5517a58B3fB9D9143697996802], amount: 500000000000000000000 [5e20])</p><p>emit SharesMinted(to: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 499999999833333333388 [4.999e20])</p><p>Now the problem was very clear.</p><h3>Breaking the invariant</h3><p>So what was happening?</p><p>In most cases when a user deposited USDC into the YieldCoin contracts, the following function would be called to:</p><ol><li>deposit their USDC to the yield generating strategy</li><li>return the totalValue of the system to calculate their YieldCoin mint amount</li></ol><pre>/// @notice Deposits USDC to the strategy and returns the total value of the system<br>/// @param amount The amount of USDC to deposit<br>/// @return totalValue The total value of the system<br>function _depositToStrategyAndGetTotalValue(uint256 amount) internal returns (uint256 totalValue) {<br>    address strategyPool = _getStrategyPool();<br>    _depositToStrategy(strategyPool, amount);<br>    totalValue = _getTotalValueFromStrategy(strategyPool);<br>    emit DepositCompleted(strategyPool, amount, totalValue);<br>}</pre><p>The totalValue, or “YieldCoin TVL”, is all of the deposits + generated yield in the system. This value is used alongside the user’s usdcDepositAmount and the totalShares, or YieldCoin total crosschain supply, to calculate the shareMintAmount (the amount of YieldCoin to mint to the user).</p><p>The incorrect order of these lines meant that the usdcDepositAmount was being included with the totalValue because the _depositToStrategy() call was happening before the _getTotalValueFromStrategy() call.</p><pre>_depositToStrategy(strategyPool, amount);<br>totalValue = _getTotalValueFromStrategy(strategyPool);</pre><p>This means the totalValue being returned by this function, used to calculate the amount of YieldCoin to mint to a depositor, was more than it should’ve been, resulting in a lower YieldCoin mint amount, losing user funds - very bad!</p><h3>Fixing the invariant</h3><p>The obvious (and correct) solution was to swap the lines around.</p><pre>totalValue = _getTotalValueFromStrategy(strategyPool);<br>_depositToStrategy(strategyPool, amount);</pre><p>Even though this had been done, the invariant continued breaking.</p><p>This is the line that performs the calculation:</p><pre>shareMintAmount = <br>	(_convertUsdcToShare(usdcDepositAmount) * totalShares) <br>		/ _convertUsdcToShare(totalValue);</pre><p>The immediate fix was to subtract usdcDepositAmount from the totalValue like so:</p><pre>shareMintAmount = <br>	(_convertUsdcToShare(usdcDepositAmount) * totalShares) <br>		/ _convertUsdcToShare(totalValue - usdcDepositAmount);</pre><p>Not a particularly efficient or aesthetic fix.</p><p>Digging through the contracts revealed a single instance of the incorrect deposit and total value fetch operations also happening outside the helper function used everywhere else.</p><p>These repetitive lines of code were replaced with a call to the _depositToStrategyAndGetTotalValue() function, with the correct order of operations. The invariant was now fixed and the usdcDepositAmount no longer needed to be subtracted from the totalValue in the calculation.</p><h3>Conclusion</h3><p>The key takeaways from this are the importance of:</p><ul><li>using multiple tools for fuzzing and verification</li><li>full unit coverage never being enough</li><li>maths</li><li><a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a> code</li></ul><p><em>YieldCoin was built for the Chainlink Chromion hackathon 2025 — see the project </em><a href="https://devfolio.co/projects/yieldcoin-112a"><em>here</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d314e9dc623f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Dev notes: Debugging and vibe coding the Lens hackathon]]></title>
            <link>https://medium.com/@contractlevel/dev-notes-debugging-and-vibe-coding-the-lens-hackathon-838b999f0160?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/838b999f0160</guid>
            <category><![CDATA[ccip]]></category>
            <category><![CDATA[lenses]]></category>
            <category><![CDATA[hackathons]]></category>
            <category><![CDATA[debugging]]></category>
            <category><![CDATA[smart-contracts]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Fri, 16 May 2025 19:20:56 GMT</pubDate>
            <atom:updated>2025-05-16T19:20:56.590Z</atom:updated>
            <content:encoded><![CDATA[<h4>This is not meant to be a polished article, these are unedited notes taken whilst participating in the Lens hackathon.</h4><p>First I wanted to build a content competition smart contract and I wanted the prize distribution to be automated when the votingDeadline was reached. Unfortunately Chainlink Automation was not yet available on Lens (or Lens Sepolia). Luckily CCIP is available on Lens Sepolia.</p><p>So the idea was to send messages across ccip to a chain with Automation when a new competition is created, including the competition’s votingDeadline. And then when the votingDeadline is met, chainlink automation would send a message back through ccip to distribute the prizePool, concluding the relevant competition on lens.</p><p>I wanted to make the automation chain Base or Arbitrum because they are cheap and fast, but the only CCIP lane for Lens Sepolia was to Eth Sepolia. That’s ok, Automation is on eth sepolia.</p><p>So I built the contracts and deploy scripts, but Eth Sepolia is evm and lens is zksync evm, which means they are compiled differently and use different versions of foundry. (soon changing, zksync is now supporting/going to support classic foundry).</p><p>the project wouldnt compile in foundryup-zksync because Chainlink’s AutomationCompatible includes the cannotExecute modifier which uses tx.origin. Foundry zksync projects refused to compile with tx.origin.</p><p>So for even basic unit testing the project had to be scaled back. Initial approach is to comment out all of the CCAutomation and ccipLocalSimulator stuff, in favour of a much more basic unit test approach.</p><h3>unit testing ContentCompetition::createCompetition()</h3><p>All the revert checks went fine (except for individual tests needing local instead of state variables but whatever), and then the ccipSend() call failed.</p><p>Why?</p><p>The CCIPRouter calls the OnRamp.</p><p>onRamp.forwardFromRouter() calls this ARMProxy:</p><p><a href="https://explorer.testnet.lens.xyz/address/0x1C6e13153FdFC78793E211f557820ed86B10c36B#contract">https://explorer.testnet.lens.xyz/address/0x1C6e13153FdFC78793E211f557820ed86B10c36B#contract</a></p><p>which calls the implementation</p><p>The ARMProxy calls RMN.isCursed(bytes16 subject) - <a href="https://explorer.testnet.lens.xyz/address/0x6D89bfFCEf0661bA485AAc97F3f1B339d8F65CEF#contract">https://explorer.testnet.lens.xyz/address/0x6D89bfFCEf0661bA485AAc97F3f1B339d8F65CEF#contract</a></p><p>This is where the revert was happening, implying the lane was not operational, even though the ccip explorer said otherwise <a href="https://ccip.chain.link/status?networkType=testnet&amp;search=lens">https://ccip.chain.link/status?networkType=testnet&amp;search=lens</a></p><p><a href="https://explorer.testnet.lens.xyz/address/0x211BF55bFA331e4149bdF624722CbCDB862Ff51D#contract">https://explorer.testnet.lens.xyz/address/0x211BF55bFA331e4149bdF624722CbCDB862Ff51D#contract</a> OnRamp.forwardFromRouter:</p><pre>/// @inheritdoc IEVM2AnyOnRampClient<br>  function forwardFromRouter(<br>    uint64 destChainSelector,<br>    Client.EVM2AnyMessage calldata message,<br>    uint256 feeTokenAmount,<br>    address originalSender<br>  ) external returns (bytes32) {<br>    if (IRMN(i_rmnProxy).isCursed(bytes16(uint128(destChainSelector)))) revert CursedByRMN();</pre><p>we assert that we have set the correct chain selector</p><pre>function test_allowedSelector() public {<br>        assertEq(comp.getAllowedChainSelector(), 16015286601757825753);<br>    } // eth sepolia selector</pre><p>then we chisel bytes16(uint128(16015286601757825753)) and get 0x0000000000000000de41ba4fc9d91ad900000000000000000000000000000000</p><p>however when this gets put in the explorer for isCursed via <a href="https://explorer.testnet.lens.xyz/address/0x6D89bfFCEf0661bA485AAc97F3f1B339d8F65CEF#read">https://explorer.testnet.lens.xyz/address/0x6D89bfFCEf0661bA485AAc97F3f1B339d8F65CEF#read</a> we get “value is invalid”</p><p>This is a bytes32 value, if we shorten it to 0x0000000000000000de41ba4fc9d91ad9, isCursed returns false, which is what we want. buuuuuut the onchain ccip contracts (which we cant change) in our fork test are passing the bytes32 value. Maybe this is just an issue with foundry-zksync</p><h3>Deployment/Verification Issues</h3><p>forge create wouldn’t work because it kept trying to do something with localhost:8545. I don’t know why. changing the rpc urls didnt work.</p><p>Fortunately the deploy script worked.</p><p>Unfortunately the lens testnet block explorers don’t have api keys like etherscan so I couldnt forge verify . One of the explorers had a login with google option, but still no api key.</p><p>This testnet has a page for manually verifying: <a href="https://explorer.testnet.lens.xyz/contracts/verify?address=0xd2550fCb1C389401E9F8802e68b093fcc0595993">https://explorer.testnet.lens.xyz/contracts/verify?address=0xd2550fCb1C389401E9F8802e68b093fcc0595993</a></p><p>So I flattened my contract and encoded the constructor args and got this error zksolc error: solc versions &gt;0.8.24 are not supported, found 0.8.26.</p><p>So I tried re-building the contracts with 0.8.24 and this didnt work because the Lens IFeed interface is 0.8.26.</p><p>abi-encode constructor args 00000000000000000000000031232cb7de0dce17949ffa58e9e38eeeb367c8710000000000000000000000007f1b9ee544f9ff9bb521ab79c205d79c55250a36000000000000000000000000f5aa9fe2b78d852490bc4e4fe9ab19727dd102980000000000000000000000000000000000000000000000000000000000030d40</p><p>This was the point I gave up trying to write a “masterpiece” codebase, left the deployed contracts unverified and proceeded to vibe up a sweet neon-gaming style frontend.</p><h3>Testnet testing</h3><p>Testing on testnets introduced additional frustrations. These are the steps I took:</p><ul><li>deployed CCAutomation on Eth Sepolia</li><li>registered with chainlink automation</li><li>funded CCAutomation with LINK</li><li>deployed ContentCompetition on Lens Sepolia</li><li>set crosschain vars (CCAutomation address and eth sepolia selector)</li></ul><p>Then I realised there is no LINK token on Lens Sepolia, presumably this means CCIP fees are paid in native/GRASS. This means the crosschain functionality used for ContentCompetition needs to be rewritten. It’s annoying because an abstract CrossChain contract was used as the base for both chains, including an immutable i_link.</p><p>Redeploy ContentCompetition with an underlying forked LensCrossChain and set the crosschain vars again.</p><p>I then wrote a script for creating competitions. Everytime I ran this, i got a REVERT: zk vm halted error. I found no results online with this error, neither did ai deepsearch, but assuming its some unsupported opcode causing the error. I kept getting this error even when commenting out the entire createCompetition function. Instead of running the script, I used cast send. That worked for the alternatively deployed contract with a commented out function, but when I tried to cast send with the actual implementation I got this error: 0x7839f485000000000000000000000000000000000000000000000000042a2b5c29184000000000000000000000000000000000000000000000000000043d3c7bf8b47dfb. I’m hoping this is some CCIP related error. How hilarious would it be if it turned out to be the same error from earlier’s fork tests where it isCursed ? I bet it’s that the CCIP fee is too much for how much GRASS we are able to obtain from the faucet…</p><p>Ok let’s go through the errors in CCIP and chisel them.</p><p>we can get the error selector with bytes4(keccak256(&quot;ErrorName()&quot;))</p><pre>bytes4(keccak256(&quot;InsufficientFee(uint256,uint256)&quot;))<br>Type: bytes4<br>└ Data: 0xa458261b00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InsufficientFeeTokenAmount()&quot;))<br>Type: bytes4<br>└ Data: 0x07da6ee600000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidMsgValue()&quot;))<br>Type: bytes4<br>└ Data: 0x1841b4e100000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;UnsupportedDestinationChain(uint64)&quot;))<br>Type: bytes4<br>└ Data: 0xae236d9c00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidChainSelector(uint64)&quot;))<br>Type: bytes4<br>└ Data: 0xd9a9cd6800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;CursedByRMN(uint64)&quot;))<br>Type: bytes4<br>└ Data: 0xfdbd6a7200000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InsufficientFee(uint256,uint256)&quot;))<br>Type: bytes4<br>└ Data: 0xa458261b00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidFeeTokenAmount()&quot;))<br>Type: bytes4<br>└ Data: 0x23e7378500000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidFeeTokenAmount(uint256,uint256)&quot;))<br>Type: bytes4<br>└ Data: 0x2c75cab400000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidExtraArgs(bytes)&quot;))<br>Type: bytes4<br>└ Data: 0xffec404a00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidMessage(uint256,uint256)&quot;))<br>Type: bytes4<br>└ Data: 0xeff5312c00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidFeeToken(address)&quot;))<br>Type: bytes4<br>└ Data: 0xeddf07f500000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;FailedToSendValue()&quot;))<br>Type: bytes4<br>└ Data: 0xe417b80b00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidRecipientAddress(address)&quot;))<br>Type: bytes4<br>└ Data: 0x26a78f8f00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;OffRampMismatch(uint64,address)&quot;))<br>Type: bytes4<br>└ Data: 0x4964779000000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;BadARMSignal()&quot;))<br>Type: bytes4<br>└ Data: 0xc148371500000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;UnsupportedDestinationChain(uint64)&quot;))<br>Type: bytes4<br>└ Data: 0xae236d9c00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InsufficientFeeTokenAmount()&quot;))<br>Type: bytes4<br>└ Data: 0x07da6ee600000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidMsgValue()&quot;))<br>Type: bytes4<br>└ Data: 0x1841b4e100000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;OnlyOffRamp()&quot;))<br>Type: bytes4<br>└ Data: 0xd2316ede00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidExtraArgsTag()&quot;))<br>Type: bytes4<br>└ Data: 0x5247fdce00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;ExtraArgOutOfOrderExecutionMustBeTrue()&quot;))<br>Type: bytes4<br>└ Data: 0xee433e9900000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;OnlyCallableByOwnerOrAdmin()&quot;))<br>Type: bytes4<br>└ Data: 0xfbdb8e5600000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;OnlyCallableByOwnerOrAdminOrNop()&quot;))<br>Type: bytes4<br>└ Data: 0x195db95800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidWithdrawParams()&quot;))<br>Type: bytes4<br>└ Data: 0x232cb97f00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;NoFeesToPay()&quot;))<br>Type: bytes4<br>└ Data: 0x8d0f71d800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;NoNopsToPay()&quot;))<br>Type: bytes4<br>└ Data: 0x990e30bf00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InsufficientBalance()&quot;))<br>Type: bytes4<br>└ Data: 0xf4d678b800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;TooManyNops()&quot;))<br>Type: bytes4<br>└ Data: 0xb5a10cfa00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;MaxFeeBalanceReached()&quot;))<br>Type: bytes4<br>└ Data: 0xe5c7a49100000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;MessageTooLarge(uint256,uint256)&quot;))<br>Type: bytes4<br>└ Data: 0x8693378900000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;MessageGasLimitTooHigh()&quot;))<br>Type: bytes4<br>└ Data: 0x4c4fc93a00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;UnsupportedNumberOfTokens()&quot;))<br>Type: bytes4<br>└ Data: 0x4c056b6a00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;UnsupportedToken(address)&quot;))<br>Type: bytes4<br>└ Data: 0xbf16aab600000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;MustBeCalledByRouter()&quot;))<br>Type: bytes4<br>└ Data: 0x1c0a352900000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;RouterMustSetOriginalSender()&quot;))<br>Type: bytes4<br>└ Data: 0xa4ec747900000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidConfig()&quot;))<br>Type: bytes4<br>└ Data: 0x35be3ac800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;CursedByRMN()&quot;))<br>Type: bytes4<br>└ Data: 0x53ad11d800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;LinkBalanceNotSettled()&quot;))<br>Type: bytes4<br>└ Data: 0x02075e0000000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidNopAddress(address)&quot;))<br>Type: bytes4<br>└ Data: 0x4de938d100000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;NotAFeeToken(address)&quot;))<br>Type: bytes4<br>└ Data: 0xa7499d2000000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;CannotSendZeroTokens()&quot;))<br>Type: bytes4<br>└ Data: 0x5cf0444900000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;SourceTokenDataTooLarge(address)&quot;))<br>Type: bytes4<br>└ Data: 0x36f536ca00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidChainSelector(uint64)&quot;))<br>Type: bytes4<br>└ Data: 0xd9a9cd6800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;GetSupportedTokensFunctionalityRemovedCheckAdminRegistry()&quot;))<br>Type: bytes4<br>└ Data: 0x9e7177c800000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidDestBytesOverhead(address,uint32)&quot;))<br>Type: bytes4<br>└ Data: 0x24ecdc0200000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;PriceNotFoundForToken(address)&quot;))<br>Type: bytes4<br>└ Data: 0x9a655f7b00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;OnlyCallableByAdminOrOwner()&quot;))<br>Type: bytes4<br>└ Data: 0xf6cd562000000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;BucketOverfilled()&quot;))<br>Type: bytes4<br>└ Data: 0x9725942a00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;RateLimitExceeded()&quot;))<br>Type: bytes4<br>└ Data: 0xa74c1c5f00000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;MessageTooLarge(uint256,uint256)&quot;))<br>Type: bytes4<br>└ Data: 0x8693378900000000000000000000000000000000000000000000000000000000</pre><p>didn’t find the error from that. let’s look at Tenderly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3aY32zxJ3xNrEgmvINMBfQ.png" /></figure><p>Tenderly shows the tx reverting a few steps after interacting with the PriceRegistry and what is presumably the native token (GRASS).</p><p>Well I increased the amount of GRASS balance the content competition contract has and the createCompetition function worked. Never found out exactly what the custom error corresponded too but whatever.</p><p>The amount of GRASS needed to send a message from Lens to Eth Sepolia via CCIP was 0.3. This was annoying because without sybil-attacking the alchemy GRASS faucet, only 0.5 GRASS could be dripped per 72 hours. There’s another faucet that requires google login which wasn’t working, but only gave 0.1 per 24 hours. And another that required solving an intricate maze for 0.1 per 24 hours too. I would’ve used realistic values for the deadlines for the first tx had I considered this a bit more, because now we need to wait to get more testnet funds to execute a bidirectional crosschain competition completion.</p><h3>Lens Mainnet Deployment</h3><p>Someone asked in the discord if submissions needed to be deployed on lens mainnet or if the testnet was fine. The mod responded that submissions were required to be on mainnet. Seems like a bad idea because obviously projects won’t be sufficiently audited for bugs in time. It also makes reliably automating prize distribution impossible because CCIP is only available on lens testnet and not on lens mainnet. Oh well maybe I’ll just deploy on the mainnet anyway — .</p><h3>Refactoring for mainnet/single chain deployment</h3><p>Forked Lens Mainnet and removed all of the CCIP stuff from Competitions.sol.</p><p>Now we’re writing some tests and actually trying to create a post on the Lens mainnet global Feed contract. This is how it’s going:</p><pre>function _createPost() internal returns (uint256 postId) {<br>        CreatePostParams memory postParams = CreatePostParams({<br>            author: author,<br>            contentURI: &quot;&lt;https://example.com&gt;&quot;,<br>            repostedPostId: 0,<br>            quotedPostId: 0,<br>            repliedPostId: 0,<br>            ruleChanges: new RuleChange[](0),<br>            extraData: new KeyValue[](0)<br>        });<br>        KeyValue[] memory customParams = new KeyValue[](0);<br>        RuleProcessingParams[] memory feedRulesParams = new RuleProcessingParams[](0);<br>        RuleProcessingParams[] memory rootPostRulesParams = new RuleProcessingParams[](0);<br>        RuleProcessingParams[] memory quotedPostRulesParams = new RuleProcessingParams[](0);</pre><pre>        postId = IFeed(feed).createPost(<br>            postParams, customParams, feedRulesParams, rootPostRulesParams, quotedPostRulesParams<br>        );<br>    }</pre><p>This is reverting with this custom error 0x3bfe162b. So now we’re back to looking through another contract and chiseling errors.</p><pre>➜ bytes4(keccak256(&quot;UnexpectedValue()&quot;))<br>Type: bytes4<br>└ Data: 0x123146a600000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;CannotHaveRules()&quot;))<br>Type: bytes4<br>└ Data: 0x3793ca3000000000000000000000000000000000000000000000000000000000<br>➜ bytes4(keccak256(&quot;InvalidMsgSender()&quot;))<br>Type: bytes4<br>└ Data: 0x3bfe162b00000000000000000000000000000000000000000000000000000000</pre><p>Haha, that was easy. D’oh. Creating the post worked after adding _changePrank(author) before the call.</p><p>The contract was successfully deployed to Lens Mainnet but because we are using the Lens Feed, which is using 0.8.26, it couldn’t be verified on the explorer <a href="https://explorer.lens.xyz/contracts/verify?address=0xd2550fCb1C389401E9F8802e68b093fcc0595993">https://explorer.lens.xyz/contracts/verify?address=0xd2550fCb1C389401E9F8802e68b093fcc0595993</a> and it definitely wasn’t for lack of trying.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gafhoelJlMj_qI7VuFiwnQ.png" /></figure><p>Verifying smart contracts on block explorer is always the worst part of development.</p><p>Doesn’t look like lenscan even has an option for verifying <a href="https://lenscan.io/address/0xd2550fCb1C389401E9F8802e68b093fcc0595993?tab=contract">https://lenscan.io/address/0xd2550fCb1C389401E9F8802e68b093fcc0595993?tab=contract</a></p><h3>Conclusion</h3><p>This project was built for fun and a lot of it ended up not being very fun. I just submitted and then realised the video included an outdated bit of code missing a crucial revert check in the distributePrizePool() func. See the video here: <a href="https://www.youtube.com/watch?v=uFq8fUUIt0g&amp;t=138s">https://www.youtube.com/watch?v=uFq8fUUIt0g&amp;t=138s</a> Meh. Doesn’t look like the submission can be edited and the code in the actual contract isnt missing the check, and the logic has been verified with Certora. Hopefully noone will notice. If everything had to be absolutely perfect, then nothing would get done. There are always improvements to make.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=838b999f0160" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Problem with Contract Level Compliance]]></title>
            <link>https://coinsbench.com/the-problem-with-contract-level-compliance-f81528e55866?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/f81528e55866</guid>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[regulatory-compliance]]></category>
            <category><![CDATA[sybil-resistance]]></category>
            <category><![CDATA[smart-contracts]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Sun, 30 Mar 2025 16:54:56 GMT</pubDate>
            <atom:updated>2025-04-02T12:42:14.472Z</atom:updated>
            <content:encoded><![CDATA[<p><em>In our </em><a href="https://medium.com/@contractlevel/identity-data-requirements-for-contract-level-compliance-b58a27c24f54"><em>previous article</em></a>,<em> we discussed the identity data requirements for </em><strong><em>Contract Level Compliance (CLC)</em></strong><em>. To briefly recap: the data that enables Contract Level Compliance must be Absolutely Sybil-resistant and provided by a licensed entity. Absolute Sybil-resistance refers to the impossibility of one person posing as multiple, and a licensed entity refers to an entity that is authorized by regulators to facilitate KYC. Now, we will discuss the problem with how CLC can currently be implemented, due to the availability of required data.</em></p><p>As of now, <a href="https://developer.everest.org/#everest-identity-oracle">Everest</a> is the only <em>known</em> provider of such data to smart contracts. <em>If anyone knows another data provider to the Chainlink network that meets the same requirements as Everest, please get in touch! contractlevel@proton.me</em></p><h3>The Risk of Dependency</h3><h4>What happens if Everest ceases operations or declines to provide its data?</h4><p>Contract Level Compliance is currently beholden to a single source for Absolutely Sybil-resistant and compliant identity data. This reliance introduces a single point of failure, meaning there is no guarantee of 100%, 24/7 access to the required data for CLC. Compromises can be made, but ultimately this reality is holding the full potential of smart contracts back, and raises a critical question:</p><h3><em>How can we decentralize Absolute Sybil-resistance and regulatory compliance?</em></h3><p><em>The following is an incomplete and hypothetical thought-experiment based on the problem of having to rely on Everest’s current form to enable Contract Level Compliance.</em></p><p>An Absolute Sybil-resistance (ASR) network of nodes guaranteeing that the private key of an address is formed from the unique biometric data of a user is evidently required.</p><p>This address could then be linked to an onchain registry of licensed entities that facilitate KYC compliance, each potentially offering their specific “level” of regulation (e.g., jurisdiction, license type, etc.). Participating providers would be required to verify their authorization by regulators, and end users would be free to complete KYC with their qualifying entity of choice.</p><p>The execution of compliant smart contracts would still be facilitated by CLC infrastructure. Requests for confirmation of a user’s compliance status could be fulfilled by whichever participating licensed entity the user chose.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SwNCYaHaqyvSYxQ_6seC_Q.jpeg" /><figcaption>Hypothetical high level overview. Maybe the end user wouldn’t interact directly with the Registry, but instead sign a message of confirmation, via the ASR Network, for their provider.</figcaption></figure><p>CLC Registered Providers would essentially bear the legal responsibility of approving a user’s participation in regulated DeFi. Devs wanting to build regulated smart contracts would restrict their logic to a chosen tier of compliance available from one or more providers. End users would complete KYC with their chosen provider, confirming with the CLC Registry via the ASR Network.</p><p>The ASR Network must be similar to traditional wallets in that multiple public addresses can be derived from the base (biometric based) private key, allowing users to maintain privacy between regulated contracts. Nodes in the network would increasingly adopt new forms of biometric scans as the technology progresses.</p><p><strong>All participating addresses in regulated contracts would be (privately) ASR linked back to a single, unique human, who has completed KYC with at least one licensed entity.</strong></p><blockquote>“If you want to participate in X smart contract, you must have been confirmed by the CLC Registry as having completed KYC with Y entity, via the ASR Network.”</blockquote><p>Compensation for CLC Registered Providers and ASR Network node operators would be necessary, with fees sourced either from end users or the regulated contracts interacting with the CLC Registry.</p><h3>The Problem with this Approach</h3><p>Consider the following scenario: There is a unique human verified by the ASR Network. This person completes KYC with Provider A and Provider B, using the passports of different people. How, then, does the CLC Registry know with 100% certainty that the uniquely verified human completed KYC correctly? Would the KYC provider need its own biometric verification system linked to the ASR Network with ZKPs? Perhaps this indicates an inherent, required inseparability of Sybil-resistance and licensed KYC checks that the above proposition fails to reconcile.</p><blockquote>Regardless of regulatory compliance, Absolute Sybil-resistance (ASR) by Consensus needs to be solved — if only to restore order from the chaos of AI participation in society.</blockquote><h3>An Honest Reflection</h3><p>Decentralizing Absolute Sybil-resistance (and regulatory compliance) is not only key to <em>reliably</em> achieving compliance at the (public) contract level, but it is also a much deeper problem than Contract Level Compliance initially set out to solve.</p><p>CLC was conceived and built with the current tooling available to smart contracts. While it can be completed and launched, its dependency on Everest’s current form prevents it from guaranteeing 100%, 24/7 reliability without additional tools resembling what has been discussed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/884/1*7-h540VY3TSzEy6oRspE4A.jpeg" /></figure><h3>The Path to ASR by Consensus</h3><h4>Everest</h4><p>The fastest path to <em>Absolute Sybil-resistance by Consensus </em>could be reached by Everest. Its identity infrastructure already achieves Absolute Sybil-resistance; the next step is decentralization, establishing itself as indispensable to smart contract ecosystems like CLC.</p><p>This is obviously not a path that can be relied upon. Therefore if Everest does not pursue this, others must — because <em>ASR by Consensus</em> is critical for Contract Level Compliance to be a viable, long-term product.</p><h4>Fuzzy Extractors</h4><p>A more reliable path may be rebuilding the entire identity infrastructure from scratch. Preliminary research indicates the use of <a href="https://www.cs.bu.edu/~reyzin/fuzzy.html">fuzzy extractors</a>, a cryptographic technique for deriving keys from biometrics, as a potential starting point.</p><h3>Moving Forward</h3><p>Research into <em>Absolute Sybil-resistance (ASR) by Consensus </em>and ideas discussed will be ongoing. Meanwhile, the crosschain vision for Contract Level Compliance, as laid out in the whitepaper, will be completed with full, comprehensive test suites and formal verification. Proof-of-concept demonstrations showcasing use of CLC’s current form are also in progress.</p><p>If you have any thoughts on how Contract Level Compliance can be improved or how the concepts discussed in this article can be achieved, please don’t hesitate to get in touch!</p><p><em>contractlevel@proton.me | x.com/contractlevel</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f81528e55866" width="1" height="1" alt=""><hr><p><a href="https://coinsbench.com/the-problem-with-contract-level-compliance-f81528e55866">The Problem with Contract Level Compliance</a> was originally published in <a href="https://coinsbench.com">CoinsBench</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Identity Data Requirements for Contract Level Compliance]]></title>
            <link>https://coinsbench.com/identity-data-requirements-for-contract-level-compliance-b58a27c24f54?source=rss-59e7d64f8d75------2</link>
            <guid isPermaLink="false">https://medium.com/p/b58a27c24f54</guid>
            <category><![CDATA[identity]]></category>
            <category><![CDATA[compliance]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[smart-contracts]]></category>
            <category><![CDATA[blockchain]]></category>
            <dc:creator><![CDATA[Contract Level]]></dc:creator>
            <pubDate>Tue, 18 Mar 2025 15:02:03 GMT</pubDate>
            <atom:updated>2025-04-02T12:42:11.897Z</atom:updated>
            <content:encoded><![CDATA[<p><em>This article refers to compliant identity data requirements from the perspective of publicly-accessible smart contracts on a public chain, as distinguished from private bank chains that have their own internal compliance procedures, and/or siloed, institutional smart contracts on public chains that have pre-approved clientele, also via their own internal compliance procedures.</em></p><p>For <strong>Contract Level Compliance (CLC) </strong>to function as intended (ie securely enable KYC checks at the contract level publicly), the data used by the system must meet certain requirements. These requirements are dictated by both the open, public nature of blockchains, and the regulating authorities from the outside world.</p><p>Public-chain compliant identity data requires three things:</p><ul><li><strong>Absolute Sybil-resistance</strong></li><li><strong>Licensed Provider</strong></li><li><strong>Chainlink</strong></li></ul><h3>Absolute Sybil-resistance</h3><p>Sybil-resistance refers to the limiting of a single actor’s ability to pose as multiple actors in a system. There are many so-called onchain identity solutions masquerading as “Sybil-resistant,” that are in fact nothing more than a series of extra steps a sufficiently determined attacker has to take to exploit. This is why we are using the term <em>Absolute Sybil-resistance</em> to explain what is required of identity data for Contract Level Compliance.</p><p>Absolute Sybil-resistance means it is as impossible for a single actor to pose as multiple actors in a system, as it is to reverse time. For Contract Level Compliance, this means an onchain user account must be non-transferable. For public chains, the only way to ensure this is using biometric data (and a secret PIN) as a private key. Biometrics are unique to each individual, making them the most reliable way to ensure one person corresponds to one private key/account. Otherwise someone could share their private key, enabling money laundering or identity fraud. This means biometrics must be scanned <em>everytime</em> an onchain action is performed, otherwise the transaction cannot be considered compliant at the contract level.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yul0aSMdU4_YP2N5qHA8NA.jpeg" /></figure><p>To illustrate this further, an example of a broken solution is one that labels an onchain user account “Sybil-resistant” based on “activity” like number of transactions executed by that account. This is broken for two reasons: the first being a single actor could create multiple accounts with activity that meets whatever arbitrary threshold determines “Sybil-resistance” in such a system. The second reason is that the private key for such a broken-system-approved account can literally be copy and pasted to somebody else. That somebody else can then use that account, determined “Sybil-resistant” based on the first person’s activity, alongside their own, thus nullifying any notion of Sybil-resistance.</p><p>Another broken solution is one that scans a user’s biometrics once to create an account or mint a “soulbound” NFT. This is broken for exactly the same reason as the second given for the previous example.</p><p>These credential-based methods of “Sybil-resistance” cannot be considered <em>Absolutely Sybil-resistant</em> because they are not. They also, as previously mentioned, enable identity fraud and money laundering, so they definitely cannot be used for Contract Level Compliance.</p><h3>Licensed Provider</h3><p>The identity data must be provided by an entity that is licensed by regulators to facilitate Know Your Customer (KYC) processes. A simple “yes” to the question “Has this user completed KYC?” is meaningless unless the entity providing that answer has been officially authorized by financial regulators to conduct such checks and proven to comply with all relevant legal obligations, including:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C8IAqPL23sqx7ExamSBwNg.jpeg" /></figure><ul><li><strong>Regulatory Compliance</strong>: Adherence to Anti-Money Laundering (AML) regulations and Financial Action Task Force (FATF) recommendations.</li><li><strong>Customer Identification</strong>: Verification of identities through the collection of personal data.</li><li><strong>Risk-Based Approach</strong>: Assessment of customer risk and application of enhanced due diligence (EDD) for high-risk cases.</li><li><strong>Ongoing Monitoring</strong>: Continuous tracking of transactions to identify suspicious activity.</li><li><strong>Record Keeping</strong>: Retaining KYC data and transaction records for the legally required time.</li><li><strong>Suspicious Activity Reporting</strong>: Reporting suspicious behavior to authorities.</li><li><strong>Data Protection</strong>: Ensuring security and customer rights in compliance with privacy laws like GDPR.</li><li><strong>Internal Controls</strong>: Establishment of KYC policies and staff training.</li><li><strong>Sanctions Screening</strong>: Checking customers against sanctions lists.</li><li><strong>Customer Due Diligence</strong>: Performing standard or simplified due diligence based on risk.</li></ul><p>These obligations ensure that identity data is not only accurate but also legally defensible, meeting the standards regulators require for financial transactions on public chains.</p><p>Data provided by a licensed entity ensures these obligations are met, otherwise the transaction cannot be considered compliant at the contract level.</p><h3>Chainlink</h3><p>The offchain to onchain delivery mechanism of the identity data must be performed by Chainlink because Chainlink is the most secure, decentralized method of doing so.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1ONtZmsF07a0F4iGwTzqbA.jpeg" /></figure><p>Chainlink’s decentralized oracle network eliminates single points of failure, ensuring that identity data remains trustworthy and untampered during its transition from offchain to onchain. If Chainlink is not used to get the data onchain, then the possibility of the data being compromised during the delivery process goes from zero to more than zero. Any shred of doubt in the integrity of the data means it cannot be considered compliant.</p><h3>Conclusion</h3><p>All of these requirements — <strong>Absolute Sybil-resistance</strong>, <strong>Licensed Provider</strong>, and <strong>Chainlink</strong>— must be met to preserve the integrity of the identity data and the CLC system itself. These are high-level invariants; compromising any one of them renders the data non-compliant, and undermines the ability of Contract Level Compliance to fulfill its mission statement. Together, they pave the way for broader adoption of smart contracts in regulated industries.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b58a27c24f54" width="1" height="1" alt=""><hr><p><a href="https://coinsbench.com/identity-data-requirements-for-contract-level-compliance-b58a27c24f54">Identity Data Requirements for Contract Level Compliance</a> was originally published in <a href="https://coinsbench.com">CoinsBench</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>