Skip to content

Commit 1c8c6d9

Browse files
committed
Merge remote-tracking branch 'origin/master' into talos
2 parents 5b73f81 + 3de581a commit 1c8c6d9

38 files changed

Lines changed: 4117 additions & 72 deletions

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Located in `contracts/strategies/`. Each strategy:
107107
- Is registered with a vault and allocated collateral
108108

109109
Key strategies: Aave, Compound, Convex/Curve, Balancer, Morpho, Native Staking (SSV validators).
110+
- For SSV Cluster migrations to ETH billing, use the SSV ETH payment calculator: https://ssv-eth-forecasting.vercel.app/
110111

111112
### OTokens
112113
`contracts/token/OUSD.sol` and `contracts/token/OETH.sol` - rebasing ERC-20 tokens. OUSD rebases to all holders; OETH uses a similar mechanism for ETH-denominated yield.

brownie/runlogs/2026_04_strategist.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,48 @@ def main():
6565
print("Pool OETH ", "{:.6f}".format(oethPoolBalance / 10**18), oethPoolBalance * 100 / totalPool)
6666
print("Pool Total ", "{:.6f}".format(totalPool / 10**18), totalPool)
6767
print("OETH Curve prices before and after", "{:.6f}".format(weth_out_before / 10**18), "{:.6f}".format(weth_out_after / 10**18))
68+
69+
# -------------------------------------------------------------
70+
# Apr 7, 2026 - Withdraw from Compounding Staking Strategy
71+
# -------------------------------------------------------------
72+
73+
from world import *
74+
75+
def main():
76+
with TemporaryForkForReallocations() as txs:
77+
# Before
78+
txs.append(vault_oeth_core.rebase(std))
79+
txs.append(oeth_vault_value_checker.takeSnapshot(std))
80+
81+
# Withdraw ETH from Compounding Staking Strategy
82+
txs.append(
83+
vault_oeth_admin.withdrawFromStrategy(
84+
COMPOUNDING_STAKING_SSV_STRAT,
85+
[WETH],
86+
[987.161129 * 10**18],
87+
{'from': STRATEGIST}
88+
)
89+
)
90+
91+
# Deposit WETH back to Compounding Staking Strategy so it can be deposited to 6 validators
92+
txs.append(
93+
vault_oeth_admin.depositToStrategy(
94+
COMPOUNDING_STAKING_SSV_STRAT,
95+
[WETH],
96+
[192 * 10**18],
97+
std
98+
)
99+
)
100+
101+
# After
102+
vault_change = vault_oeth_core.totalValue() - oeth_vault_value_checker.snapshots(STRATEGIST)[0]
103+
supply_change = oeth.totalSupply() - oeth_vault_value_checker.snapshots(STRATEGIST)[1]
104+
profit = vault_change - supply_change
105+
106+
txs.append(oeth_vault_value_checker.checkDelta(profit, (1 * 10**18), vault_change, (1 * 10**18), std))
107+
108+
print("-----")
109+
print("Profit", "{:.6f}".format(profit / 10**18), profit)
110+
print("OETH supply change", "{:.6f}".format(supply_change / 10**18), supply_change)
111+
print("Vault Change", "{:.6f}".format(vault_change / 10**18), vault_change)
112+
print("-----")
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import { AbstractSafeModule } from "./AbstractSafeModule.sol";
5+
import { IVault } from "../interfaces/IVault.sol";
6+
7+
/**
8+
* @title PermissionedRebaseModule
9+
* @notice Safe module that lets a permissioned operator drive a `rebase()`
10+
* on vaults that are kept in the rebase-paused state. For each
11+
* configured vault, the module calls (in order, via the Safe):
12+
* `unpauseRebase()` -> `rebase()` -> `pauseRebase()`.
13+
* If any sub-call fails, the whole transaction reverts so a vault
14+
* can never be left unpaused by a partial run.
15+
*/
16+
contract PermissionedRebaseModule is AbstractSafeModule {
17+
mapping(address => bool) public isVaultWhitelisted;
18+
address[] public vaults;
19+
20+
event VaultAdded(address vault);
21+
event VaultRemoved(address vault);
22+
event PermissionedRebaseExecuted(address vault);
23+
24+
constructor(
25+
address _safeAddress,
26+
address _operator,
27+
address[] memory _vaults
28+
) AbstractSafeModule(_safeAddress) {
29+
_grantRole(OPERATOR_ROLE, _operator);
30+
31+
for (uint256 i = 0; i < _vaults.length; i++) {
32+
_addVault(_vaults[i]);
33+
}
34+
}
35+
36+
/**
37+
* @notice For every whitelisted vault, sequentially call
38+
* `unpauseRebase()`, `rebase()`, then `pauseRebase()` via the
39+
* Safe. Reverts atomically on any sub-call failure.
40+
*/
41+
function permissionedRebase() external onlyRole(OPERATOR_ROLE) {
42+
uint256 vaultsLength = vaults.length;
43+
for (uint256 i = 0; i < vaultsLength; i++) {
44+
address vault = vaults[i];
45+
_execOnVault(vault, IVault.unpauseRebase.selector);
46+
_execOnVault(vault, IVault.rebase.selector);
47+
_execOnVault(vault, IVault.pauseRebase.selector);
48+
emit PermissionedRebaseExecuted(vault);
49+
}
50+
}
51+
52+
function _execOnVault(address vault, bytes4 selector) internal {
53+
bool success = safeContract.execTransactionFromModule(
54+
vault,
55+
0, // Value
56+
abi.encodeWithSelector(selector),
57+
0 // Call
58+
);
59+
require(success, "Vault call failed");
60+
}
61+
62+
/**
63+
* @notice Add a vault to the whitelist. Only the Safe can call.
64+
*/
65+
function addVault(address _vault) external onlyRole(DEFAULT_ADMIN_ROLE) {
66+
_addVault(_vault);
67+
}
68+
69+
function _addVault(address _vault) internal {
70+
require(_vault != address(0), "Vault is zero address");
71+
require(!isVaultWhitelisted[_vault], "Vault already whitelisted");
72+
isVaultWhitelisted[_vault] = true;
73+
vaults.push(_vault);
74+
emit VaultAdded(_vault);
75+
}
76+
77+
/**
78+
* @notice Remove a vault from the whitelist. Only the Safe can call.
79+
*/
80+
function removeVault(address _vault) external onlyRole(DEFAULT_ADMIN_ROLE) {
81+
require(isVaultWhitelisted[_vault], "Vault not whitelisted");
82+
isVaultWhitelisted[_vault] = false;
83+
84+
for (uint256 i = 0; i < vaults.length; i++) {
85+
if (vaults[i] == _vault) {
86+
vaults[i] = vaults[vaults.length - 1];
87+
vaults.pop();
88+
break;
89+
}
90+
}
91+
92+
emit VaultRemoved(_vault);
93+
}
94+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
/**
5+
* @title IHydrexGauge
6+
* @notice Minimal interface exposing the staked-token getter used by the
7+
* Hydrex GaugeV2 (>= v2.5). Hydrex renamed `TOKEN()` to `stakeToken()`
8+
* in v2.5; the rest of the gauge surface (deposit / withdraw /
9+
* getReward / emergency / emergencyWithdraw / balanceOf) is
10+
* ABI-compatible with `IAlgebraGauge` and is invoked through that
11+
* interface elsewhere in the strategy.
12+
*/
13+
interface IHydrexGauge {
14+
function stakeToken() external view returns (address);
15+
}

contracts/contracts/proxies/Proxies.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,10 @@ contract OUSDMorphoV2StrategyProxy is InitializeGovernedUpgradeabilityProxy {
249249
contract OETHSupernovaAMOProxy is InitializeGovernedUpgradeabilityProxy {
250250

251251
}
252+
253+
/**
254+
* @notice OETHbHydrexAMOProxy delegates calls to an OETHbHydrexAMOStrategy implementation
255+
*/
256+
contract OETHbHydrexAMOProxy is InitializeGovernedUpgradeabilityProxy {
257+
258+
}

contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pragma solidity ^0.8.0;
77
* @author Origin Protocol Inc
88
*/
99
import { StableSwapAMMStrategy } from "./StableSwapAMMStrategy.sol";
10+
import { IGauge } from "../../interfaces/algebra/IAlgebraGauge.sol";
1011

1112
contract OETHSupernovaAMOStrategy is StableSwapAMMStrategy {
1213
/**
@@ -15,6 +16,6 @@ contract OETHSupernovaAMOStrategy is StableSwapAMMStrategy {
1516
* @param _gauge Address of the Supernova gauge for the pool.
1617
*/
1718
constructor(BaseStrategyConfig memory _baseConfig, address _gauge)
18-
StableSwapAMMStrategy(_baseConfig, _gauge)
19+
StableSwapAMMStrategy(_baseConfig, _gauge, IGauge(_gauge).TOKEN())
1920
{}
2021
}

contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,18 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy {
159159
* @param _baseConfig The `platformAddress` is the address of the Algebra pool.
160160
* The `vaultAddress` is the address of the Origin Vault.
161161
* @param _gauge Address of the Algebra gauge for the pool.
162+
* @param _gaugeStakeToken The pool LP token address as reported by the
163+
* gauge. The inheriting contract is expected to resolve this via
164+
* whichever getter its gauge exposes (e.g. `IGauge.TOKEN()` for
165+
* legacy GaugeV2 ≤ v2.4 or `IHydrexGauge.stakeToken()` for
166+
* Hydrex GaugeV2 ≥ v2.5) and pass the result here. The constructor
167+
* verifies it matches `_baseConfig.platformAddress`.
162168
*/
163-
constructor(BaseStrategyConfig memory _baseConfig, address _gauge)
164-
InitializableAbstractStrategy(_baseConfig)
165-
{
169+
constructor(
170+
BaseStrategyConfig memory _baseConfig,
171+
address _gauge,
172+
address _gaugeStakeToken
173+
) InitializableAbstractStrategy(_baseConfig) {
166174
// Read the oToken address from the Vault
167175
address oTokenMem = IVault(_baseConfig.vaultAddress).oToken();
168176
address assetMem = IVault(_baseConfig.vaultAddress).asset();
@@ -178,9 +186,11 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy {
178186
IPair(_baseConfig.platformAddress).isStable() == true,
179187
"Pool not stable"
180188
);
181-
// Check the gauge is for the pool
189+
// Check the gauge is wired to the expected pool LP token. The
190+
// inheriting contract is responsible for fetching `_gaugeStakeToken`
191+
// from whichever getter the underlying gauge variant exposes.
182192
require(
183-
IGauge(_gauge).TOKEN() == _baseConfig.platformAddress,
193+
_gaugeStakeToken == _baseConfig.platformAddress,
184194
"Incorrect gauge"
185195
);
186196
oTokenPoolIndex = IPair(_baseConfig.platformAddress).token0() ==
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
/**
5+
* @title OETHb Hydrex Algorithmic Market Maker (AMO) Strategy
6+
* @notice AMO strategy for the Hydrex superOETHb/WETH stable pool on Base
7+
* @author Origin Protocol Inc
8+
*/
9+
import { StableSwapAMMStrategy } from "../algebra/StableSwapAMMStrategy.sol";
10+
import { IHydrexGauge } from "../../interfaces/hydrex/IHydrexGauge.sol";
11+
12+
contract OETHbHydrexAMOStrategy is StableSwapAMMStrategy {
13+
/**
14+
* @param _baseConfig The `platformAddress` is the address of the Hydrex superOETHb/WETH pool.
15+
* The `vaultAddress` is the address of the OETHBase Vault.
16+
* @param _gauge Address of the Hydrex gauge for the pool. Hydrex GaugeV2
17+
* (>= v2.5) renamed `TOKEN()` to `stakeToken()`, which is what we
18+
* resolve here and forward to the parent.
19+
*/
20+
constructor(BaseStrategyConfig memory _baseConfig, address _gauge)
21+
StableSwapAMMStrategy(
22+
_baseConfig,
23+
_gauge,
24+
IHydrexGauge(_gauge).stakeToken()
25+
)
26+
{}
27+
}

contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pragma solidity ^0.8.0;
77
* @author Origin Protocol Inc
88
*/
99
import { StableSwapAMMStrategy } from "../algebra/StableSwapAMMStrategy.sol";
10+
import { IGauge } from "../../interfaces/algebra/IAlgebraGauge.sol";
1011

1112
contract SonicSwapXAMOStrategy is StableSwapAMMStrategy {
1213
/**
@@ -15,6 +16,6 @@ contract SonicSwapXAMOStrategy is StableSwapAMMStrategy {
1516
* @param _gauge Address of the SwapX gauge for the pool.
1617
*/
1718
constructor(BaseStrategyConfig memory _baseConfig, address _gauge)
18-
StableSwapAMMStrategy(_baseConfig, _gauge)
19+
StableSwapAMMStrategy(_baseConfig, _gauge, IGauge(_gauge).TOKEN())
1920
{}
2021
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const { deployOnBase } = require("../../utils/deploy-l2");
2+
const addresses = require("../../utils/addresses");
3+
const {
4+
deployOETHbHydrexAMOStrategyImplementation,
5+
} = require("../deployActions");
6+
7+
module.exports = deployOnBase(
8+
{
9+
deployName: "048_oethb_hydrex_amo",
10+
},
11+
async ({ deployWithConfirmation, ethers }) => {
12+
// 1. Deploy the OETHb Hydrex AMO proxy
13+
await deployWithConfirmation("OETHbHydrexAMOProxy");
14+
const cOETHbHydrexAMOProxy = await ethers.getContract(
15+
"OETHbHydrexAMOProxy"
16+
);
17+
18+
// 2. Deploy & initialize the strategy implementation against the live
19+
// Hydrex gauge configured in addresses.base.HydrexOETHb_WETH.gauge.
20+
const cOETHbHydrexAMOStrategy =
21+
await deployOETHbHydrexAMOStrategyImplementation(
22+
addresses.base.HydrexOETHb_WETH.gauge
23+
);
24+
25+
// 3. Connect to the OETHBase Vault as IVault
26+
const cOETHBaseVaultProxy = await ethers.getContract("OETHBaseVaultProxy");
27+
const cVault = await ethers.getContractAt(
28+
"IVault",
29+
cOETHBaseVaultProxy.address
30+
);
31+
32+
// 4. Connect to the OETHBase harvester proxy. Use the same harvester
33+
// that AerodromeAMOStrategy uses (OETHHarvesterSimple via the
34+
// OETHBaseHarvesterProxy) so reward token flows go through the
35+
// standard Origin harvester pipeline.
36+
const cHarvesterProxy = await ethers.getContract("OETHBaseHarvesterProxy");
37+
const cHarvester = await ethers.getContractAt(
38+
"OETHHarvesterSimple",
39+
cHarvesterProxy.address
40+
);
41+
42+
return {
43+
name: "Deploy OETHb Hydrex AMO Strategy on Base",
44+
actions: [
45+
// Approve the strategy on the OETHBase Vault
46+
{
47+
contract: cVault,
48+
signature: "approveStrategy(address)",
49+
args: [cOETHbHydrexAMOProxy.address],
50+
},
51+
// Allow the strategy to mint OETHb via the Vault
52+
{
53+
contract: cVault,
54+
signature: "addStrategyToMintWhitelist(address)",
55+
args: [cOETHbHydrexAMOProxy.address],
56+
},
57+
// Set the harvester address on the strategy. Rewards (oHYDX) flow
58+
// strategy → harvester → strategist via OETHHarvesterSimple's
59+
// harvestAndTransfer.
60+
{
61+
contract: cOETHbHydrexAMOStrategy,
62+
signature: "setHarvesterAddress(address)",
63+
args: [cHarvesterProxy.address],
64+
},
65+
// Mark the strategy as supported on the harvester so
66+
// harvestAndTransfer(strategy) doesn't revert.
67+
{
68+
contract: cHarvester,
69+
signature: "setSupportedStrategy(address,bool)",
70+
args: [cOETHbHydrexAMOProxy.address, true],
71+
},
72+
],
73+
};
74+
}
75+
);

0 commit comments

Comments
 (0)