Skip to content

feat: add AgentHub onboarding helpers and shared test base - #1

Closed
samtin0x wants to merge 3 commits into
mainfrom
feat/agent-hub-onboarding-helpers
Closed

samtin0x wants to merge 3 commits into
mainfrom
feat/agent-hub-onboarding-helpers

Conversation

@samtin0x

@samtin0x samtin0x commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Extracts the agent onboarding logic, parameters and assertions that a proposal currently re-implements, so onboarding the next asset is a market address and an eMode id rather than a copy of the last proposal.

Taken from AaveV3Ethereum_Onboard_PTsrUSDe22OCT2026_Oracle_20260817 (aave-dao/aave-proposals-v3#1184), the first agent onboarding, which carries all of it inline.

Additive: seven new files, nothing existing modified.

What a proposal repeats today

The payload is 143 lines: 13 constants, a 44-line execute(), and two internal functions. Nine of those constants and both internal functions have nothing to do with the asset being onboarded — onboarding a second PT changes the market address and the eMode ids, and copies the rest verbatim. Its test file is another 302 lines, of which five of seven tests assert properties of the onboarding rather than of the asset.

Before — every proposal writes this:

uint256 public constant DISCOUNT_MINIMUM_DELAY = 2 days;
uint256 public constant EMODE_MINIMUM_DELAY = 3 days;
uint256 public constant DISCOUNT_EXPIRATION_PERIOD = 2 days;
uint256 public constant EMODE_EXPIRATION_PERIOD = 3 days;
uint120 public constant DISCOUNT_RANGE_ABS = 1e16;
uint120 public constant EMODE_RANGE_ABS_BPS = 50;
string public constant DISCOUNT_UPDATE_TYPE = 'PendleDiscountRateUpdate';
string public constant EMODE_UPDATE_TYPE = 'EModeCategoryUpdate';
string public constant UPDATE_TYPE_SUFFIX = '';

function execute() external {
  // ...
  uint256 discountAgentId = _registerAgentAndGrantRole(
    discountRateAgent,
    DISCOUNT_EXPIRATION_PERIOD,
    DISCOUNT_MINIMUM_DELAY,
    string.concat(DISCOUNT_UPDATE_TYPE, UPDATE_TYPE_SUFFIX),
    bytes(''),
    ptMarkets
  );

  _setDefaultRange(discountAgentId, string.concat(DISCOUNT_UPDATE_TYPE, UPDATE_TYPE_SUFFIX), DISCOUNT_RANGE_ABS);
  _setDefaultRange(eModeAgentId, 'EModeLTV', EMODE_RANGE_ABS_BPS);
  _setDefaultRange(eModeAgentId, 'EModeLiquidationThreshold', EMODE_RANGE_ABS_BPS);
  _setDefaultRange(eModeAgentId, 'EModeLiquidationBonus', EMODE_RANGE_ABS_BPS);
}

/// 25 lines spelling out all 13 fields of AgentRegistrationInput
function _registerAgentAndGrantRole(...) internal returns (uint256) { ... }

/// 15 lines wrapping RangeConfig
function _setDefaultRange(...) internal { ... }

After — the proposal keeps the agent addresses, the market and the eModes, and nothing else:

uint256 discountAgentId = AgentHubOnboarding.registerAgentAndGrantRiskAdmin(
  AaveV3Ethereum.ACL_MANAGER,
  MiscEthereum.AGENT_HUB,
  AgentHubOnboarding.AgentOnboardingInput({
    agentAddress: DISCOUNT_RATE_AGENT,
    riskOracle: LLAMARISK_RISK_ORACLE,
    admin: AGENT_ADMIN,
    updateType: LlamaRiskPTAgentConfig.discountUpdateType(),
    expirationPeriod: LlamaRiskPTAgentConfig.DISCOUNT_EXPIRATION_PERIOD,
    minimumDelay: LlamaRiskPTAgentConfig.DISCOUNT_MINIMUM_DELAY,
    agentContext: bytes(''),
    allowedMarkets: ptMarkets
  })
);

AgentHubOnboarding.setAbsoluteRange(
  RANGE_MODULE, MiscEthereum.AGENT_HUB, discountAgentId,
  LlamaRiskPTAgentConfig.discountUpdateType(), LlamaRiskPTAgentConfig.DISCOUNT_RANGE_ABS
);

AgentHubOnboarding.setAbsoluteRanges(
  RANGE_MODULE, MiscEthereum.AGENT_HUB, eModeAgentId,
  LlamaRiskPTAgentConfig.eModeBoundedUpdateTypes(), LlamaRiskPTAgentConfig.EMODE_RANGE_ABS_BPS
);

Both internal functions and nine constants leave the proposal. The test file inherits AgentHubTestBase and keeps only the asset-specific assertions.

Why it is worth extracting rather than copying

Two of the repeated pieces fail quietly when copied wrong.

AgentRegistrationInput has 13 fields and 7 have been identical in every onboarding, among them isAgentPermissioned and restrictedMarkets. Spelling all 13 out per proposal puts two permission-bearing flags in front of an author with no reason to touch them, in a struct where a wrong value is a permission granted rather than a compile error.

A fresh agent id inherits no range config, and RangeValidationModule reads a missing config as a zero bound. An agent registered without one is registered and permanently unable to inject — nothing reverts at execution, and the failure only surfaces when the first update is rejected. setAbsoluteRanges exists because the eMode agent registers under one update type but injects three fields, each bounded separately, so bounding three of four is the same silent failure.

The update type strings have a third version of this problem. The RiskOracle is constructed with them, the agents publish under them, and the AgentHub registers against them, so three layers agree only because the same literal is written in three repositories. A rename breaks that silently: registration still succeeds and nothing published is ever matched.

What this adds

src/agent-hub/AgentHubOnboarding.sol — registerAgentAndGrantRiskAdmin grants RISK_ADMIN and registers in one call, fixing the 7 defaults and taking the 6 fields that vary, returning the id the hub allocated rather than letting a caller assume it. setAbsoluteRange / setAbsoluteRanges set symmetric absolute bounds; absolute because a relative bound is a percentage of a last injected value a fresh id does not have.

src/agent-hub/AgentHubTestBase.sol — the assertions that are not asset-specific: stored configuration matches what the payload declared, the ACL role was granted, and every update type an agent writes was bounded rather than only some.

src/agent-hub/LlamaRiskPTAgentConfig.sol — the onboarding parameters shared across the PT agents.

src/interfaces/IAgentHub.sol, IAgentConfigurator.sol, IRangeValidationModule.sol — copied unmodified from aave-dao/aave-agent-hub @ 71e6dbb.

Interfaces

aave-helpers does not depend on aave-agent-hub, so these signatures do not exist here and the helpers cannot compile without them. The files are vendored verbatim rather than narrowed: each is byte-identical to its source, so they diff clean against upstream and a version bump is a re-copy rather than a re-narrowing. AgentRegistrationInput is ABI encoded by position, so a copy trimmed by hand could reorder a field, compile, and encode wrongly against the live hub. Prettier leaves all three unchanged, so CI will not reformat them out of sync.

Declared rather than added as a submodule, for three reasons:

  1. This repository already does this. src/interfaces/IChainlinkAggregator.sol and IPermissionedPayloadsController.sol are copies of interfaces from non-dependencies.
  2. aave-proposals-v3 already made this call for these exact contracts. Its src/interfaces/ holds its own IAgentHub.sol and IAgentConfigurator.sol, alongside equivalents for Umbrella, GHO and the Chainlink robots — and it does pull submodules, so the alternative was available and not taken.
  3. The dependency tree here is deliberately narrow. Two submodules, forge-std and aave-address-book; aave-v3-origin, aave-v4, OpenZeppelin and solidity-utils all arrive through the address book. A third would be inherited by every consumer, including aave-proposals-v3, for two calls and a handful of getters, and would pin this repository to a version of one still moving.

The argument for a submodule is drift. The tests address it more directly: they run against the live AgentHub rather than a mock, so an upstream struct change fails them immediately, where a pin held at an older commit would keep passing while being wrong.

If maintainers would rather take the submodule, the interfaces delete cleanly and the helpers are unaffected.

Testing

Helpers run against a pinned mainnet fork rather than mocks, so the struct is filled the way the live hub reads it back. Config values are checked against the deployed stack rather than the proposal source: the RiskOracle at 0x8346170dcE5455A1205f55A0b5448E67e42CD270 reports exactly ["PendleDiscountRateUpdate", "EModeCategoryUpdate"].

forge test --match-path "tests/agent-hub/*"    13 passed, 0 failed
forge build                                    successful
prettier --check                               clean

Notes for review

  • The onboarding authority is AaveV3Ethereum.ACL_ADMIN, verified on mainnet as both the ACL default admin and the AgentHub owner. EXECUTOR_LVL_1 is neither.
  • Both helpers make more than one call, so tests pranking them need startPrank rather than prank.
  • A proposal genuinely needing a non-default registration flag should call registerAgent directly and say why, rather than this library growing a parameter for it.
  • LlamaRiskPTAgentConfig is provider-specific configuration, which nothing in this repository currently holds. It is here because the values are shared across proposals rather than across providers, but placement and naming are worth a maintainer's view — it sits equally well in aave-risk-agents.
  • Changeset included.

Follow-ups

Once released, aave-dao/aave-proposals-v3#1184 drops its nine constants and both internal functions and inherits the test base.

Onboarding a risk agent is two calls a proposal cannot land separately:
granting the agent the ACL role it injects with, and registering it in
the AgentHub. Every proposal so far has re-implemented both, along with
the assertions that check they worked.

`AgentRegistrationInput` has thirteen fields, seven of which have been
identical in every onboarding. Spelling all thirteen out per proposal
puts `isAgentPermissioned` and `restrictedMarkets` in front of an author
with no reason to change them, in a struct where a wrong flag is a
permission granted rather than a compile error. `AgentHubOnboarding`
fixes those seven and takes the six that vary.

`setAbsoluteRange` covers the other half. A fresh agent id inherits no
range config, and the module reads a missing config as a zero bound, so
an agent registered without one is registered and permanently unable to
inject. Bounds are absolute because a relative bound is measured against
a last injected value a fresh id does not have.

`AgentHubTestBase` carries the assertions that follow from the above:
that the stored configuration matches what the payload declared, that
the ACL role was granted, and that every update type an agent writes was
bounded rather than only some.

`IAgentHub` and `IRangeValidationModule` are narrowed locally, as
`IChainlinkAggregator` and `IPermissionedPayloadsController` already are,
rather than taking `aave-agent-hub` as a dependency.

Tested against a pinned mainnet fork rather than mocks, so the struct is
filled the way the live hub reads it back: 7 passed.
The onboarding parameters are the other half of what every PT proposal
copies: two update types, two delays, two expiration periods and two
bounds, none of which vary per asset. Onboarding a second PT changes the
market address and the eMode ids and nothing here.

The update type strings are the ones worth moving. The RiskOracle is
constructed with them, the agents publish under them, and the AgentHub
registers against them, so the three layers agree only if the same
literal is written in three repositories. Reading them from one place
makes that structural rather than a matter of typing carefully: a rename
currently breaks the agreement silently, since registration still
succeeds and nothing published is ever matched.

`eModeBoundedUpdateTypes` is a function rather than three constants
because the eMode agent registers under one update type and injects
three fields, each bounded separately. A field omitted from that list is
a field left unbounded, which the module reads as a zero bound and uses
to reject every update.

Values verified against the live stack rather than copied from the
proposal: the RiskOracle at 0x8346170dcE5455A1205f55A0b5448E67e42CD270
reports exactly ["PendleDiscountRateUpdate", "EModeCategoryUpdate"].

13 passed across the agent-hub suites.
The previous versions were hand narrowed to the calls the helpers make,
which left a copy that had to be re-read against upstream to be trusted:
`AgentRegistrationInput` is ABI encoded by position, so a field reordered
while trimming would compile and encode wrongly.

Copied unmodified from aave-dao/aave-agent-hub @ 71e6dbb instead, so the
files diff clean against their source and a bump is a re-copy rather than
a re-narrowing. `IAgentConfigurator` comes with them, since `IAgentHub`
inherits it there.

Verified identical: sha256 of each file matches its source, and prettier
leaves all three unchanged, so CI will not reformat them out of sync.

13 passed.
@samtin0x
samtin0x force-pushed the feat/agent-hub-onboarding-helpers branch from c2347d8 to edb7923 Compare August 20, 2026 12:10

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not necessary, please remove

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

removing - q; changeset only added if the npm package is affected? or how does it work?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we can imwhy do we need these interfaces?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why we need this interface?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why we need this interface?

///
/// A proposal that genuinely needs a non-default flag should call `registerAgent` directly and
/// say why, rather than this library growing a parameter for it.
/// @author Aave Labs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lets keep the natspecs not too long and focussed,

@samtin0x

Copy link
Copy Markdown
Collaborator Author

Config logic stays in the proposal rather than moving to aave-helpers, interfaces get vendored in aave-proposals-v3 under src/interfaces (which the AIP already does), and the address book only needs the deployed addresses.

Leaving the branch up as a follow-up for the agents-repo abstraction

@samtin0x samtin0x closed this Aug 20, 2026
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.

2 participants