Skip to content

Migrate proportional action arguments from bps to ppm - #617

Open
e1Ru1o wants to merge 3 commits into
masterfrom
e1Ru1o/ppm
Open

Migrate proportional action arguments from bps to ppm#617
e1Ru1o wants to merge 3 commits into
masterfrom
e1Ru1o/ppm

Conversation

@e1Ru1o

@e1Ru1o e1Ru1o commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

Breaking change. Every action argument that expresses a proportion of Settler's token balance is re-denominated from basis points (BASIS = 10_000) to parts-per-million (BASIS = 1_000_000), and renamed bpsppm (maxBpsmaxPpm in POSITIVE_SLIPPAGE). This makes sub-bp proportions expressible (e.g. a 30 ppm fee) and stops small fee proportions from truncating to zero on large-notional trades.

Action selectors are unchanged (parameter names are not part of the selector), so the unit is a property of the Settler deployment: encoders must key bps vs ppm on the Settler address they target.

Details

  • BASIS becomes 1_000_000 in SettlerAbstract, Encoder, and Decoder (the constructor cross-check asserts are unchanged and still hold).
  • The packed proportion field widens from 2 bytes to 3 bytes in two places:
    • the non-VIP flash-accounting header (Encoder.encode / Decoder.initialize in FlashAccountingCommon.sol); VIP headers carry no proportion and are untouched;
    • the leading field of every fill in UNISWAPV4, BALANCERV3, PANCAKE_INFINITY, EKUBO, and EKUBOV3. _HOP_DATA_LENGTH grows by 1 per venue (52→53, 3→4, 82→83, 47→48, 47→48).
  • Flag bits that ride on the fill proportion move to the new MSBs: BalancerV3 wrap/unwrap bits 15/14 → 23/22 (masks 0xc00000/0x400000/0x3fffff), Ekubo forwarding-extension bit 15 → 23 (0x800000/0x7fffff).
  • The Permit2 balance-proportional sell amount sentinel (~amount < BASIS) and the RFQ maker-permit asserts widen automatically with the constant.
  • The [Unreleased] CHANGELOG entry documents the full encoding change for integrators; sh/initial_description_*.md are updated so future chain bring-ups register ppm wording (existing chains' feature descriptions are immutable one-shot IPFS metadata and are unaffected).

Deliberately unchanged

Pool fees carried in the upper 16 bits of swapInfo (UniswapV2, Velodrome, VelodromeAlt) stay denominated in bps against a hard-coded 10_000: they describe the external pool's own fee convention, and 1e6 does not fit in that 16-bit field. Likewise EulerSwap's 1e4 LTV scale and MakerPSM's GEM_basis.

Testing

  • Unit tests: 189/189 pass via the CI command, including the UniswapV4 suite exercising the 3-byte fill encoding (single/multihop/multiplex/diamond/VIP) against a real PoolManager.
  • All unit and integration tests migrated: proportional values ×100, fill encoders uint16/bytes2uint24/bytes3, flag constructions moved to bits 23/22.
  • FOUNDRY_PROFILE=integration compiles; fork tests + gas comparison to be confirmed by CI.
  • Per-chain forge build --sizes passes for every chain in chain_config.json.

Gas Optimization

No hot-path changes; the arithmetic is identical (* ppm / BASIS). Calldata grows 1 byte per fill plus 1 byte per flash-accounting action. Contract size deltas vs master (95184a2) on the tightest chain:

Contract master this PR Δ headroom
MainnetSettler 23,619 23,625 +6 B 951 B
MainnetSettlerIntent 22,487 22,491 +4 B 2,085 B
MainnetSettlerMetaTxn 21,661 21,660 −1 B 2,916 B
MainnetBridgeSettler 6,340 6,345 +5 B 18,231 B

The growth is the PUSH2PUSH3 widening of the BASIS literal and flag masks.

🤖 Generated with Claude Code

e1Ru1o and others added 2 commits July 30, 2026 19:26
BASIS changes from 10_000 to 1_000_000. Every action argument expressing a
proportion of Settler's token balance is renamed bps->ppm (maxBps->maxPpm) and
re-denominated in parts-per-million. Action selectors are unchanged.

The packed proportion field widens from 2 to 3 bytes in the flash-accounting
header and in each UNISWAPV4/BALANCERV3/PANCAKE_INFINITY/EKUBO/EKUBOV3 fill;
per-venue _HOP_DATA_LENGTH grows by 1. The BalancerV3 wrap/unwrap flags move
to bits 23/22 of the fill's proportion field and the Ekubo forwarding-extension
flag to bit 23. The Permit2 balance-proportional sentinel band widens with
BASIS.

Pool fees carried in swapInfo (UniswapV2, Velodrome) remain denominated in bps
against a hard-coded 10_000 per the pools' own convention, as does EulerSwap's
1e4 LTV scale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@e1Ru1o
e1Ru1o requested a review from jparklev July 31, 2026 11:03
@e1Ru1o e1Ru1o self-assigned this Jul 31, 2026

@jparklev jparklev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one.

Should we mention in the README like, "the first ppm deployment on each chain is nonce N (per-chain table), earlier nonces are bps”? Not 100% whats best for integrators, but seems like a pretty large behavoiral change we wouldn't want anyone to opt into accidentally

(
address(fromToken()),
10_000,
1_000_000,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add some sub-bp cases here to test? (eg the 30 ppm fee mentioned by Phil)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might, but:

  • ideally I would keep the proportions of all tests to not break them.
  • I don't think it brings that much value as tests should already test different proportions

using NotesLib for NotesLib.Note[];

uint256 internal constant BASIS = 10_000;
uint256 internal constant BASIS = 1_000_000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on one shared like PPM_DENOM constant we import? (should be runtime bytecode neutral)

(in theory we also then wouldn't have to do like assert(BASIS == Encoder.BASIS) in BalancerV3 and pancake constructors)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, at some point we should get to have a Constants.sol for things like this and native for example

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me. Maybe add it to a Constants.sol file where we also put the ERC7528 constant?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol. I am slow.

Comment thread src/core/FlashAccountingCommon.sol Outdated
// implicitly advance it by 20 bytes to decode `ppm` then advance by 23 bytes

bps := shr(0x50, calldataload(data.offset))
ppm := shr(0x48, calldataload(data.offset))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider uint256 ppm; + ppm := shr(0xe8, calldataload(add(0x14, data.offset))).

It’s a couple bytes smaller, and then we don't need the “implicitly advance by 20 bytes” comment

(similar in erc20 branch below. could punt to a future cleanup)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is a good catch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the original implementation that created a dirty bps was more gas/stack efficient because it avoided an extra calldataload. But this is relatively old code that was written for an older solc that might be dumber than the current one.

It's worth testing.

Comment thread CHANGELOG.md Outdated
### Breaking changes

* All proportional-amount action arguments are now denominated in parts-per-million (`ppm`; denominator 1_000_000) instead of basis points (`bps`; denominator 10_000)
* Affects the third-ish argument of `UNISWAPV3`, `UNISWAPV2`, `BASIC`, `VELODROME`, `MAKERPSM`, `DODOV1`, `DODOV2`, `MAVERICKV2`, `EULERSWAP`, `HANJI`, `UNISWAPV4`, `BALANCERV3`, `PANCAKE_INFINITY`, `EKUBO`, and `EKUBOV3`; the `maxBps` (now `maxPpm`) argument of `POSITIVE_SLIPPAGE`; and BridgeSettler's `BASIC`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"third-ish" reads as kind of odd here to me. Maybe just named like "the proportion-of-balance argument"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed it to bps to be explicit about the argument affected instead of its position

@e1Ru1o
e1Ru1o marked this pull request as ready for review August 12, 2026 10:30
@e1Ru1o
e1Ru1o requested a review from duncancmt as a code owner August 12, 2026 10:30
@immunefi-magnus

Copy link
Copy Markdown

🛡️ Immunefi PR Reviews

We noticed that your project isn't set up for automatic code reviews. If you'd like this PR reviewed by the Immunefi team, you can request it manually using the link below:

🔗 Send this PR in for review

Once submitted, we'll take care of assigning a reviewer and follow up here.

using NotesLib for NotesLib.Note[];

uint256 internal constant BASIS = 10_000;
uint256 internal constant BASIS = 1_000_000;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me. Maybe add it to a Constants.sol file where we also put the ERC7528 constant?

using NotesLib for NotesLib.Note[];

uint256 internal constant BASIS = 10_000;
uint256 internal constant BASIS = 1_000_000;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol. I am slow.

Comment thread src/core/FlashAccountingCommon.sol Outdated
// implicitly advance it by 20 bytes to decode `ppm` then advance by 23 bytes

bps := shr(0x50, calldataload(data.offset))
ppm := shr(0x48, calldataload(data.offset))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the original implementation that created a dirty bps was more gas/stack efficient because it avoided an extra calldataload. But this is relatively old code that was written for an older solc that might be dumber than the current one.

It's worth testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants