Our synthetic router deposits 90 asset units into a vault and receives 60 share units. It then stakes those 60 shares for the original caller. Passing 90 into the farm because that was the deposit amount creates an accounting error before any complicated reward calculation begins.

The example is an independent JavaScript model of Solidity contract composition. It represents a fixed exchange ratio solely to expose the unit boundary. There is no deployment configuration or external vault implementation behind the numbers and no assumption that a real vault keeps the same exchange ratio over time.

Name the quantity at every interface

An asset balance measures what the vault holds. A share balance measures a claim against those assets. A farm balance measures the shares credited to a participant inside another contract. Those values may use the same integer type in Solidity while representing three different obligations.

We make the conversion explicit using a ratio of three asset units for every two shares. The rounding policy floors issued shares. A deposit returning zero shares must fail in our model because accepting assets without a positive claim violates the chosen deposit policy.

function depositAndStake(beneficiary, assets) {
  const shares = (assets * shareDenominator) / assetNumerator;
  if (shares === 0n) throw new Error("zero shares");
  stakes.set(beneficiary, (stakes.get(beneficiary) ?? 0n) + shares);
  return shares;
}
function unstakeAndRedeem(beneficiary, shares) {
  const staked = stakes.get(beneficiary) ?? 0n;
  if (shares > staked) throw new Error("insufficient stake");
  stakes.set(beneficiary, staked - shares);
  return (shares * assetNumerator) / shareDenominator;
}

A production vault can calculate the conversion from total assets and total shares. It also needs rules for its empty state and for donations that change its balance without issuing shares. We do not import those mechanisms into this fixture because the router question begins after the vault establishes its own accounting rule.

Keep the original beneficiary across the call chain

When a router calls another contract the downstream contract sees the router as its caller. A farm that blindly credits that caller records the position under the router. The user then owns no direct farm position even though their assets funded the transaction.

We pass the beneficiary as an explicit argument and require the router to preserve it through the composition. That convenience adds an authorization obligation because a routable withdrawal must establish whose position is being reduced and who is allowed to request the operation. An arbitrary beneficiary parameter does not supply permission by itself.

The router has to preserve both the meaning of each integer and the identity of the participant entitled to the resulting position because a transaction that moves the right number of tokens into the wrong account can satisfy every token transfer check while leaving the user's accounting claim somewhere else.

Withdraw the change produced by this operation

Our reverse path unstakes 20 shares and redeems them for 30 asset units. We place an unrelated 11 asset units in the router before the operation. Returning the router's entire balance transfers 41 units and gives the caller assets belonging to another accounting context.

The fixture checks the amount attributable to the current redemption and leaves the prior balance untouched. In an actual integration the appropriate measurement depends on the vault interface and supported token semantics. Returned quantities need validation against the tokens received and any minimum output chosen by the caller.

A before and after balance difference can isolate the received quantity for ordinary ERC20 tokens. Rebasing tokens and callbacks introduce further assumptions. A router should either define support for those behaviors or reject assets whose accounting breaks the integration model.

90 assets -> 60 shares; 20 shares -> 30 assets
10 assertions passed

Atomic execution has a defined scope

On the EVM an uncaught revert rolls back state changes across the transaction. Our JavaScript fixture does not reproduce that rollback. Its assertions inspect successful conversions and isolated rejection checks so the model does not imply that ordinary JavaScript exceptions reverse previous mutations.

For a Solidity implementation we test a failing farm deposit after a successful vault call. The entire transaction must revert if partial completion is outside the product contract. We also test false returning tokens and allowances along with a recipient that attempts another entry point during a callback.

Changing the vault exchange ratio between operations makes a stronger integration test than assuming one asset always buys one share. The router must continue staking the actual returned shares and enforcing the caller's minimum output as the value of each share changes.

Sources and examples