chore(portfolio-contract): swap reward tokens to USDC via 1inch#12706
Conversation
147f6dd to
72ff993
Compare
dckc
left a comment
There was a problem hiding this comment.
if a more substantive review is needed from me, we should talk
| await documentStorageSchema(t, storage, docOpts); | ||
| }); | ||
|
|
||
| test('swap reward token to USDC via 1inch', async t => { |
There was a problem hiding this comment.
I trust that in due course, there will be a contract-level feature test that shows how end-users use this new code.
There was a problem hiding this comment.
I expect a contract level test to be somewhat representative, and chain a claim and a swap step. I think it's ok to postpone until the claim PR introduces such a test.
mhofman
left a comment
There was a problem hiding this comment.
I think we still have some bits to iron out on the design, but the overall direction looks good so far.
There was a problem hiding this comment.
Out of scope for this PR, but we should probably refactor some of this to use the newer tools we have to generate call data.
There was a problem hiding this comment.
More details for the future: what I'm thinking about is better separation of concerns. Generating the call info (aka target + encoded calldata, but also gasLimit and sent value for router calls) that the messaging layers care about can be layered on top of a more basic tool that builds the encoded calldata from abi / function signature. I think makeEvmContract and contractWithCallMetadata (or something like it) builds that separation, but I haven't tried to adopt it throughout the existing codebase.
| functionSignature, | ||
| args, | ||
| abi, | ||
| data, |
There was a problem hiding this comment.
nit: data seems a bit too generic. maybe something more specific like encodedData would better clarify the purpose of this arg
There was a problem hiding this comment.
i believe an update to privateArgs-ymax1.json and privateArgs-ymax0.json would be required as well
There was a problem hiding this comment.
Good catch - updated both privateArgs-ymax0.json and privateArgs-ymax1.json with oneInchRouter on all 5 chains.
| aaveUSDC: aaveUsdcAddresses.testnet.Base, | ||
| aaveRewardsController: aaveRewardsControllerAddresses.testnet.Base, | ||
| walletHelper: walletHelperAddresses.testnet.Base, | ||
| oneInchRouter, |
There was a problem hiding this comment.
i thought testnet didnt have a working oneInch contract?
## What Pin `packages/orchestration`'s chain-info codegen to a specific cosmos [chain-registry](https://github.com/cosmos/chain-registry) commit so it is deterministic, and refresh the committed chain info. ## Why The `test-codegen` job runs `scripts/verify-codegen-idempotence.mjs`, which runs `yarn codegen` in every package and fails on any resulting diff. Orchestration's codegen fetched live data from chain-registry `master`, so it was **not** a pure function of the repo: whenever upstream added IBC connections/channels, the regenerated `src/fetched-chain-info.js` diverged from the committed file and the check failed — on **unrelated** PRs, repo-wide, until someone refreshed. This is currently breaking CI on unrelated work, e.g. the `test-codegen` failure in #12706: https://github.com/Agoric/agoric-sdk/actions/runs/27113973754/job/80017490932?pr=12706 ## How - **Pin the fetch** to a specific chain-registry commit (the `CHAIN_REGISTRY_COMMIT` constant in `scripts/fetch-chain-info.ts`). `yarn codegen` fetches that immutable commit, so output is reproducible and upstream drift can no longer break CI. Fetching is still fine; drifting is not. - **`yarn codegen --refresh`** re-pins to the latest chain-registry commit (rewriting the constant in place) and regenerates, so updates are adopted deliberately. A refresh is a one-line bump to the constant plus the regenerated output. - Commit the latest chain info refreshed from the registry. - Document the refresh workflow in the package README ("Chain info" section). - Make the idempotence check's failure message explain exactly what to do, and note that codegen pulling external data must pin its source. ## Testing - `yarn codegen` (no args) reproduces `src/fetched-chain-info.js` byte-for-byte (idempotent) and leaves the script untouched. - `yarn codegen --refresh` re-pins and regenerates. - `tsc` and eslint pass on the changed files. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mhofman
left a comment
There was a problem hiding this comment.
Overall looks good, but I think there is some cleanup remaining after the switch to recomposing the call data.
Also one question about which details should be published.
There was a problem hiding this comment.
More details for the future: what I'm thinking about is better separation of concerns. Generating the call info (aka target + encoded calldata, but also gasLimit and sent value for router calls) that the messaging layers care about can be layered on top of a more basic tool that builds the encoded calldata from abi / function signature. I think makeEvmContract and contractWithCallMetadata (or something like it) builds that separation, but I haven't tried to adopt it throughout the existing codebase.
mhofman
left a comment
There was a problem hiding this comment.
The swap params should not be plumbed through the context. I also think the minReturnAmount is extraneous (I do not see a way this could be anything but the USDC amount expected to be received in the step).
Besides that looks great.
| await documentStorageSchema(t, storage, docOpts); | ||
| }); | ||
|
|
||
| test('swap reward token to USDC via 1inch', async t => { |
There was a problem hiding this comment.
I expect a contract level test to be somewhat representative, and chain a claim and a swap step. I think it's ok to postpone until the claim PR introduces such a test.
Out of date, we recompose now.
Please note that a contract level test will be added once claim support is there.
I don't understand this. If anything I believe it should be the opposite. The contract must be deployed first to support new steps that the planner may generate. |
| dest: AnyString<AssetPlaceRef>(), | ||
| }, | ||
| { | ||
| phases: M.recordOf(M.string(), M.arrayOf(M.string())), |
There was a problem hiding this comment.
did we really not have this in the type guard before? I guess it's not used but in tests...
There was a problem hiding this comment.
Yes - it was never in the shape on master (just { how, amount, src, dest }), even though phases is published. FlowStepsShape is only used in offer-shapes.test.ts; publishStatus doesn't validate against it at runtime, and the fixtures never included phases, so the gap went unnoticed.
7249712 to
72e0062
Compare
72e0062 to
4a2c5e2
Compare
closes: https://linear.app/agoric/issue/PAK-462/ymax-contract-should-be-able-to-swap-reward-tokens-to-usdc-on-1inch
Description
Adds a contract-side mechanism to swap reward tokens to USDC on EVM chains via 1Inch, as part of the reward-token auto-claiming cycle.
Security Considerations
This introduces a new authority: from the portfolio's own remote account, the contract grants the configured 1inch router an ERC20 approval and calls its
swap()entry point. It does not forward an opaque planner-supplied calldata blob — the planner supplies the swap as decomposed named fields and the contract reconstructs theswap()calldata itself, pinning the fund-safety fields it controls:srcToken= the reward token being approved,dstToken= USDC,dstReceiver= this portfolio's own remote account,amount= the approvedamountIn, andminReturnAmount= the movement amount (the USDC floor, which Zoe validates).These are correct by construction, so proceeds can only ever arrive as USDC in our own account, pulling no more than approved, and the router reverts unless at least
minReturnAmountreachesdstReceiver. The target is pinned to the configured router and the selector to 1inch V6swap().executor,srcReceiver, anddataare opaque route internals, trusted only because the planner produced them — they cannot redirect funds given the pinned fields. The allowance is reset to0after the swap so the router keeps no residual grant.Testing Considerations
Upgrade Considerations
Deployment must be sequenced contract-first: the contract must support the new
swapstep before the planner produces one. The contract upgrade is backward-compatible — it adds handling forMovementDesc.swapbut never requires it — so it can ship ahead of the planner and simply stays inert until the planner is upgraded. The reverse order would let the planner submit swaps an older contract can't process.Ordering
main-0) contract upgrade first — adds swap-step support; no behavior change until its planner produces swaps.MovementDesc.swap); validate the full reward-token → USDC swap flow end-to-end against the upgraded contract.main-1) contract upgrade, then its planner — only after end-to-end verification on ymax0 looks good.