There are currently five ARM contracts:
- Lido ARM on Ethereum with stETH as the base asset and WETH as the liquidity asset.
- EtherFi ARM on Ethereum with eETH as the base asset and WETH as the liquidity asset.
- Ethena ARM on Ethereum with sUSDe as the base asset and USDe as the liquidity asset.
- USD ARM on Ethereum with PYUSD and USDG as base assets and USDC as the liquidity asset.
- OS ARM on Sonic with OS as the base asset and wS as the liquidity asset.
See the ARM Registry for the deployed contracts.
Uniswap V2 Router compatible interface for swapping ERC20 tokens.
/**
* @notice Swaps an exact amount of input tokens for as many output tokens as possible.
* msg.sender should have already given the ARM contract an allowance of
* at least amountIn on the input token.
*
* @param inToken Input token.
* @param outToken Output token.
* @param amountIn The amount of input tokens to send.
* @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert.
* @param to Recipient of the output tokens.
* @return amounts The input and output token amounts.
*/
function swapExactTokensForTokens(
IERC20 inToken,
IERC20 outToken,
uint256 amountIn,
uint256 amountOutMin,
address to
) external returns (uint256[] memory amounts);
/**
* @notice Uniswap V2 Router compatible interface. Swaps an exact amount of
* input tokens for as many output tokens as possible.
* msg.sender should have already given the ARM contract an allowance of
* at least amountIn on the input token.
*
* @param amountIn The amount of input tokens to send.
* @param amountOutMin The minimum amount of output tokens that must be received for the transaction not to revert.
* @param path The input and output token addresses.
* @param to Recipient of the output tokens.
* @param deadline Unix timestamp after which the transaction will revert.
* @return amounts The input and output token amounts.
*/
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
/**
* @notice Receive an exact amount of output tokens for as few input tokens as possible.
* msg.sender should have already given the router an allowance of
* at least amountInMax on the input token.
*
* @param inToken Input token.
* @param outToken Output token.
* @param amountOut The amount of output tokens to receive.
* @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts.
* @param to Recipient of the output tokens.
* @return amounts The input and output token amounts.
*/
function swapTokensForExactTokens(
IERC20 inToken,
IERC20 outToken,
uint256 amountOut,
uint256 amountInMax,
address to
) external returns (uint256[] memory amounts);
/**
* @notice Uniswap V2 Router compatible interface. Receive an exact amount of
* output tokens for as few input tokens as possible.
* msg.sender should have already given the router an allowance of
* at least amountInMax on the input token.
*
* @param amountOut The amount of output tokens to receive.
* @param amountInMax The maximum amount of input tokens that can be required before the transaction reverts.
* @param path The input and output token addresses.
* @param to Recipient of the output tokens.
* @param deadline Unix timestamp after which the transaction will revert.
* @return amounts The input and output token amounts.
*/
function swapTokensForExactTokens(
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
/**
* @notice Get the available reserves for each token in the ARM.
* @dev Applies to the Lido, EtherFi, OETH and OS ARMs.
* @return reserve0 The available liquidity for token0.
* @return reserve1 The available liquidity for token1.
*/
function getReserves() external view returns (uint256 reserve0, uint256 reserve1);
/**
* @notice Get available liquidity and base asset reserves for a supported base asset.
* @dev Applies to the Ethena and USD ARMs.
* For the Ethena ARM, reserveBaseAsset is sUSDe and liquidityAssets are USDe.
* For the USD ARM, reserveBaseAsset is PYUSD or USDG and liquidityAssets are USDC.
* Liquidity assets are net of outstanding LP withdrawal claims.
* @param reserveBaseAsset Supported base asset whose reserve should be returned.
* @return liquidityAssets Available liquidity assets.
* @return baseAssetReserve Base assets held directly by the ARM.
*/
function getReserves(address reserveBaseAsset)
external
view
returns (uint256 liquidityAssets, uint256 baseAssetReserve);The ARM contract provides a Tokenized Vault Standard (ERC-4626) like interface for adding/removing liquidity to/from the ARM.
The main difference with ERC-4626 is the ARM's asynchronous withdrawal process which requests a redeem and then claims the redeem after a ten minute waiting period.
In addition, the ARM only supports deposit and redeem functions, not mint and withdraw.
/// @notice Preview the amount of shares that would be minted for a given amount of assets
/// @param assets The amount of liquidity assets to deposit
/// @return shares The amount of shares that would be minted
function previewDeposit(uint256 assets) external view returns (uint256 shares);
/// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares.
/// The caller needs to have approved the contract to transfer the assets.
/// @param assets The amount of liquidity assets to deposit
/// @return shares The amount of shares that were minted
function deposit(uint256 assets) external returns (uint256 shares);
/// @notice deposit liquidity assets in exchange for liquidity provider (LP) shares.
/// Funds will be transferred from msg.sender.
/// @param assets The amount of liquidity assets to deposit
/// @param receiver The address that will receive shares.
/// @return shares The amount of shares that were minted
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
/// @notice Preview the amount of assets that would be received for burning a given amount of shares
/// @param shares The amount of shares to burn
/// @return assets The amount of liquidity assets that would be received
function previewRedeem(uint256 shares) external view returns (uint256 assets);
/// @notice Request to redeem liquidity provider shares for liquidity assets
/// @param shares The amount of shares the redeemer wants to burn for liquidity assets
/// @return requestId The index of the withdrawal request
/// @return assets The amount of liquidity assets that will be claimable by the redeemer
function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets);
/// @notice Claim liquidity assets from a previous withdrawal request after the claim delay has passed.
/// This will withdraw from the active lending market if there are not enough liquidity assets in the ARM.
/// @param requestId The index of the withdrawal request
/// @return assets The amount of liquidity assets that were transferred to the redeemer
function claimRedeem(uint256 requestId) external returns (uint256 assets);
/// @notice Calculates the amount of shares for a given amount of liquidity assets
/// @dev Total assets can't be zero. The lowest it can be is MIN_TOTAL_SUPPLY
/// @param assets The amount of liquidity assets to convert to shares
/// @return shares The amount of shares that would be minted for the given assets
function convertToShares(uint256 assets) public view returns (uint256 shares);
/// @notice Calculates the amount of liquidity assets for a given amount of shares
/// @dev Total supply can't be zero. The lowest it can be is MIN_TOTAL_SUPPLY
/// @param shares The amount of shares to convert to assets
/// @return assets The amount of liquidity assets that would be received for the given shares
function convertToAssets(uint256 shares) public view returns (uint256 assets);
/// @notice The total amount of assets in the ARM, active lending market and external withdrawal queue,
/// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees.
/// @return The total amount of assets in the ARM
function totalAssets() public view virtual returns (uint256);
/// @notice The liquidity asset used for deposits and redeems. eg WETH or wS
/// Used for compatibility with ERC-4626
/// @return The address of the liquidity asset
function asset() external view virtual returns (address);The ARM token can be used as collateral on lending platforms to borrow other assets. The ARM token can not be borrowed on lending platforms as the ARM's price, or assets per share, can be increased by donating assets to the ARM. eg donating WETH or stETH to the Lido ARM will increase the ARM's token price. Increasing the price of a borrowed asset increases the borrower's loan to value (LTV) ratio which can lead to a liquidation.
make install
cp .env.example .env
In the .env file, set the environment variables as needed. eg MAINNET_URL for the RPC endpoint.
make
Fork and Unit tests run with the same command, as the fork is initiated on the test file itself if needed.
By default:
- verbosity is set to 3, i.e. are displayed logs, assertion errors (expected vs actual), and stack traces for failing tests.
- a summary of the test is displayed at the end.
To run all tests:
make test
To run only a test contract:
make test-c-TestContractName
To run only a specific test
make test-f-TestFunctionName
Report gas usage for tests:
make gas
We use ERC-2335 to encode private key. To be sure to never reveal it, we use the cast wallet PK management.
cast wallet import deployerKey --interactiveIn the .env file, set DEPLOYER_ADDRESS (should match the privateKey you registered wit cast wallet import deployerKey) to an account that has funds on the Tenderly Testnet.
Set TESTNET_URL to the Tenderly Testnet RPC endpoint.
make deploy-testnetIn the .env file, set DEPLOYER_ADDRESS (should match the privateKey you registered wit cast wallet import deployerKey), ETHERSCAN_API_KEY and MAINNET_URL to the mainnet values.
make deploy-mainnetIn the .env file, set DEPLOYER_ADDRESS (should match the privateKey you registered wit cast wallet import deployerKey) and SONIC_URL to the mainnet values.
make deploy-sonicIf the verification doesn't work with the deployment, it can be done separately with forge verify-contract.
For example
# Verify LidoARM
forge verify-contract 0xeC6FdCc3904F8dD6a9cbbBCC41B741df5963B42E LidoARM \
--constructor-args $(cast abi-encode "constructor(address,address,address,uint256,uint256,int256)" 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1 600 0 0 )
# Verify Proxy
forge verify-contract 0x6bac785889A4127dB0e0CeFEE88E0a9F1Aaf3cC7 Proxy
# Verify Proxy on Sonic
forge verify-contract --chain 146 --etherscan-api-key $SONICSCAN_API_KEY 0x2F872623d1E1Af5835b08b0E49aAd2d81d649D30 Proxy
To upload a deployed contract to Tenderly, set TENDERLY_ACCESS_TOKEN in your
environment.
# Sync all deployment aliases from the current chain's deployment file
yarn hardhat tenderlySync --network mainnet
yarn hardhat tenderlySync --network sonic
# Upload a deployment alias from the current chain's deployment file
yarn hardhat tenderlyUpload --network sonic --name ORIGIN_ARMThe src/js/tasks/actions/*.ts files are hardhat tasks that handle operational jobs (allocations, fee collection, withdrawal requests, etc.). In production they're driven by a container that imports @talos/client:
runner.tsat repo root callsrunContainer({ product: "arm-oeth", workdir: "/app" }). The library reads enabled schedules from the shared Talos Postgres, fires them via croner, and spawns each schedule's command aspnpm hardhat <name> --network <chain>.migrations/seed_schedules.sqlis a one-time seed of theschedulestable mirroring the oldcron/cron-jobs.ts.src/js/tasks/lib/action.tswraps the hardhat signer withwrapSignerWithNonceQueueV6from the library whenDATABASE_URLis set. That routessigner.sendTransactionthrough Postgres row-locked nonce coordination.
Every scheduled action — its cadence and one-line purpose — is catalogued in docs/ACTIONS.md.
Every action remains directly executable as a hardhat task on your dev machine:
pnpm hardhat allocateLido --network mainnet
pnpm hardhat healthcheckNo Postgres required. The library's nonce queue is gated by process.env.DATABASE_URL: if unset, the action uses a raw ethers signer with the provider's default nonce handling — exactly the behavior you had before Automaton existed. The gate is a single check at the top of the handler; no DB connection is opened.
If you opt in by setting DATABASE_URL (e.g., via docker compose up), the nonce queue engages and will try to connect to whatever that URL points at. unset DATABASE_URL to go back to the unwrapped path.
Signer selection (DEPLOYER_PRIVATE_KEY → KMS via KMS_RELAYER_ID → IMPERSONATE → hardhat first signer) lives in src/js/utils/signers.ts.