Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b950321
build: instant acrual
Schlagonia Apr 24, 2026
ac6c8fd
build: unlock and accrue
Schlagonia Apr 24, 2026
f0c3265
test: report locking
Schlagonia Apr 25, 2026
81d4fc1
test: losses
Schlagonia Apr 25, 2026
59e9599
fix: loss checks burn shares
Schlagonia Apr 26, 2026
e811205
fix: cleanups
Schlagonia Apr 26, 2026
78db3fa
chore: optimize
Schlagonia Apr 26, 2026
1ef546f
chore: init function
Schlagonia Apr 27, 2026
a9d7a77
feat: distinguish accrual events from reports
Schlagonia May 18, 2026
32b2715
fix: block transfers from strategy
Schlagonia May 18, 2026
5e54a73
fix: commeny
Schlagonia May 19, 2026
ea64f5f
feat: tokenized strategy access into library (#115)
Schlagonia May 19, 2026
3781f5e
Supply-chain hardening sweep (#113)
murderteeth May 19, 2026
1e5b2dd
chore: bump api version to 3.1.0
Schlagonia May 19, 2026
a972067
fix: accrue before updating profit unlock time
Schlagonia May 19, 2026
bfe2717
fix: expose strategy total assets view
Schlagonia May 19, 2026
be2cdb1
chore: pack struct
Schlagonia May 19, 2026
ce50b28
feat: add strategy pause control (#116)
Schlagonia May 24, 2026
729b970
chore: spech
Schlagonia Jun 1, 2026
0454726
test: add constant accrual accounting invariants (#118)
Schlagonia Jun 3, 2026
a7a1db5
feat: reentrancy
Schlagonia Jun 8, 2026
5de1a43
fix: ignore test fail
Schlagonia Jun 8, 2026
11a423a
fix: audit changes (#120)
Schlagonia Jun 12, 2026
4768430
310 fixes (#119)
Schlagonia Jun 12, 2026
4854122
chore: add default total assets (#121)
Schlagonia Jun 18, 2026
1632cb0
feat: v3.1.0 (#122)
Schlagonia Jun 19, 2026
cdcec99
chore: flatten
Schlagonia Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:

steps:
- name: Check out github repository
uses: actions/checkout@v2
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
with:
fetch-depth: 1

- name: Setup node.js
uses: actions/setup-node@v1
uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1
with:
node-version: '16.x'

Expand All @@ -27,7 +27,7 @@ jobs:
run: echo "::set-output name=dir::$(yarn cache dir)"

- name: Restore yarn cache
uses: actions/cache@v2
uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace # v2
id: yarn-cache
with:
path: |
Expand All @@ -50,9 +50,9 @@ jobs:

steps:
- name: Check out github repository
uses: actions/checkout@v2
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
with:
fetch-depth: 0

- name: Run commitlint
uses: wagoid/commitlint-github-action@v2
uses: wagoid/commitlint-github-action@4b1bcb1c72f99fbd6aa6b34cc3fb59200f01f993 # v2
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ jobs:
name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1
with:
version: nightly

Expand Down
3 changes: 3 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
npmMinimalAgeGate: 10080
enableScripts: false
defaultSemverRangePrefix: ""
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> [!CAUTION]
> **v3.1.0 risk:** share pricing is driven by `strategyTotalAssets()`. The default returns `lastTotalAssets()`, but strategies that override it for live accounting can move PPS, previews, and conversions before `report()`; if that value is wrong or manipulable, vault shares can be mispriced.

# Yearn Tokenized Strategy

Expand All @@ -7,7 +9,7 @@ The implementation address that calls are delegated to is pre-set to a constant

NOTE: The master branch has these pre-set addresses set based on the deterministic address that testing on a local device will render. These contracts should NOT be used in production and any live versions should use an official [release](https://github.com/yearn/tokenized-strategy/releases).

A Strategy contract can become a fully ERC-4626 compliant vault by inheriting the `BaseStrategy` contract, that uses the fallback function to delegateCall the previously deployed version of `TokenizedStrategy`. A strategist then only needs to override three simple functions in their specific strategy.
A Strategy contract can become a fully ERC-4626 compliant vault by inheriting the `BaseStrategy` contract, that uses the fallback function to delegateCall the previously deployed version of `TokenizedStrategy`. A strategist then only needs to override three required functions in their specific strategy, with the option to override `_strategyTotalAssets()` for live accounting.

[TokenizedStrategy](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol) - The implementation contract that holds all logic for every strategy.

Expand Down
29 changes: 19 additions & 10 deletions SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The majority of functions in the BaseStrategy are either external functions with

`harvestAndReport()/_harvestAndReport()`: Called during reports to tell the strategy a trusted address has called it and to harvest any rewards re-deploy any loose funds and return the actual amount of funds the strategy holds.

`tendThis(uint256)/_tend(uint256)`: Called by the TokenizedStrategy during tend calls to tell the strategy a trusted address has called tend and it has the uint256 parameter of loose asset available to deposit. NOTE: we use `tendThis` to avoid function signature collisions so that `tend` will be forwarded to the TokenizedStrategy.
`tendThis(uint256)/_tend(uint256)`: Called by the TokenizedStrategy during tend calls to tell the strategy a trusted address has called tend and it has the uint256 parameter of loose asset available to deposit. NOTE: we use `tendThis` to avoid function signature collisions so that `tend` will be forwarded to the TokenizedStrategy. If `_strategyTotalAssets()` is overridden for constant accrual, a tend is not an accounting boundary: value changes it makes price into views through simulated totals and are realized by the next state-changing accrual, not only by a report.

`tendTrigger()/_tendTrigger()`: View function to return if a tend call is needed.

Expand All @@ -86,10 +86,17 @@ Users can deposit ASSET tokens to receive shares.

Deposits are limited by the availableDepositLimit function that can be changed by the strategist if non uint256.max values are desired.

#### First Depositor / Donation Protection
Profit recognized by an accrual while the effective share supply is below `MINIMUM_SUPPLY` (1e3) is minted to a dead address as shares at a flat price per share rather than accruing to holders. This covers both assets that show up before the first deposit (the vault starts from a 1:1 PPS) and donations made while an attacker controls a dust supply, which bounds the classic first-depositor inflation attack: for a donation to move the share price at all the attacker must hold at least 1e3 shares of their own, capping any victim rounding loss at roughly `donation / 1e3`. Confiscated profit is not charged performance fees.

This guard lives only in the accrual path, which is the single point where unsolicited value can enter pricing for permissionless flows (every deposit, mint, withdraw and redeem accrues first, and all conversions price off accrued or simulated totals). `report()` is intentionally not guarded: it is keeper-permissioned, and with a non-zero `profitMaxUnlockTime` profit locking already prevents any instant PPS jump.

Because no shares are ever carved from depositors, deposits, mints, previews and max functions remain fully ERC-4626 compliant, there is no minimum first deposit, and a vault that is fully exited holds no locked dead shares from normal operation — dead shares only ever result from pre-deposit donations or attacker donations.

#### Withdrawals / Redeems
Users can redeem their shares at any point in time if there is liquidity available.
Users can redeem their shares when the strategy is not paused and there is liquidity available.

The amount of a withdraw or redeem can be limited by the strategist by overriding the availableWithdrawLimit function.
The amount of a withdraw or redeem can be limited by the strategist by overriding the availableWithdrawLimit function. If the strategy is paused, withdraw and redeem are blocked.

In order to properly comply with the ERC-4626 standard and still allow losses, both withdraw and redeem have an additional optional parameter of 'maxLoss' that can be used. The default for 'maxLoss' is 0 (i.e. revert if any loss) for withdraws, and 10_000 (100%) for redeems.

Expand Down Expand Up @@ -169,22 +176,22 @@ This can be customized based on the strategy. Based on aspects such as TVL, expe
Strategy Shares are ERC4626 compliant.

## Emergency Operation
There is default emergency functions built in. First of which is `shutdownStrategy`. This can only ever be called by the management address and is non-reversible.
There is default emergency functions built in. First of which is `shutdownStrategy`. This can be called by the management address or emergencyAdmin and is non-reversible.

Once this is called it will stop any further deposit or mints but will have no effect on any other functionality including withdraw, redeem, report and tend. This is to allow management to continue potentially recording profits or losses and users to withdraw even post shutdown.
Once this is called it will stop any further deposit or mints but will have no effect on any other functionality including withdraw, redeem, report and tend. This is to allow management to continue potentially recording profits or losses and users to withdraw even post shutdown. If the strategy is also paused, the pause still blocks deposit, mint, withdraw and redeem until management unpauses.

This can be used in an emergency or simply to retire a vault.

Once a strategy is shutdown management can also call `emergencyWithdraw(amount)`. Which will tell the strategy to withdraw a specified `amount` from the yield source and keep it as idle in the vault. This function will also do any needed updates to totalDebt and totalIdle, based on amounts withdrawn to assure withdraws continue to function properly.
Once a strategy is shutdown or paused, management or emergencyAdmin can also call `emergencyWithdraw(amount)`. Which will tell the strategy to withdraw a specified `amount` from the yield source and keep it as idle in the vault. This function performs no accounting update at call time, so the rescue path has no dependency on the strategy's asset estimate. Any profit or loss caused by the unwind prices into views through simulated totals and is realized by the next state-changing accrual or report.

All other emergency functionality is left up to the individual strategist.

### Withdrawals
Withdrawals can't be paused under any circumstance unless built in a specific implementation.
Withdrawals and redemptions are paused by `setPaused(true)`, which can be called by management or emergencyAdmin. Only management can unpause. Shutdown alone does not pause withdrawals or redemptions; liquidity, `availableWithdrawLimit`, and the paused state determine whether a user can withdraw.


## Use
A strategist can simply inherit the BaseStrategy.sol contract and override 3 simple functions with their specific needs.
A strategist can simply inherit the BaseStrategy.sol contract and override 3 required functions with their specific needs. They can also override `_strategyTotalAssets()` if they want live accounting instead of the default report-boundary accounting.

The strategies code has been designed as a non-opinionated system to distribute funds of depositors to a single yield generating opportunity while managing accounting in a robust way.

Expand All @@ -209,7 +216,7 @@ Example constraints:
- ...

## Development
Strategists should be able to use a pre-built "Strategy Mix" that will contain the imported BaseStrategy.sol as well as standardized tests for any 4626 vault. Developing a strategy can be as simple as overriding three functions, with the potential for any number of other constraints or actions to be built on top of it. The Base implementation is only ~2KB, meaning there is plenty of room for strategists to build complex implementations while not having to be concerned with the generic functionality.
Strategists should be able to use a pre-built "Strategy Mix" that will contain the imported BaseStrategy.sol as well as standardized tests for any 4626 vault. Developing a strategy can be as simple as overriding three required functions, with the potential for any number of other constraints or actions to be built on top of it. The Base implementation is only ~2KB, meaning there is plenty of room for strategists to build complex implementations while not having to be concerned with the generic functionality.


### Needed to Override
Expand All @@ -224,7 +231,9 @@ Strategists should be able to use a pre-built "Strategy Mix" that will contain t

While it can be possible to deploy a completely ERC-4626 compliant vault with just those three functions it does allow for further customization if the strategist desires.

*_tend* and *_tendTrigger* can be overridden to signal to keepers the need for any sort of maintenance or reward selling between reports.
*_strategyTotalAssets()*: By default this returns `TokenizedStrategy.lastTotalAssets()`, preserving report-boundary accounting. Override it only when the strategy needs live accounting from a read-only current asset estimate.

*_tend* and *_tendTrigger* can be overridden to signal to keepers the need for any sort of maintenance or reward selling between reports. If `_strategyTotalAssets()` is overridden for constant accrual, any value change made during a tend affects view pricing through simulated totals and is realized by the next state-changing accrual rather than waiting for a report.

*availableDepositLimit(address _owner)* can be overridden to implement any type of deposit limit.

Expand Down
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ out = 'out'
libs = ['lib']
solc = "0.8.18"
evm_version = "paris"
optimize = true
optimizer_runs = 200
no_match_test = "testFail"

remappings = [
'forge-std/=lib/forge-std/src/',
Expand Down
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
{
"name": "yearn_base_strategy",
"devDependencies": {
"prettier": "^2.5.1",
"prettier-plugin-solidity": "^1.0.0-beta.19",
"pretty-quick": "^3.1.3",
"prettier": "2.8.4",
"prettier-plugin-solidity": "1.1.3",
"pretty-quick": "3.1.3",
"solc": "0.8.18",
"solhint": "^3.3.7",
"solhint-plugin-prettier": "^0.0.5"
"solhint": "3.4.0",
"solhint-plugin-prettier": "0.0.5"
},
"scripts": {
"format": "prettier --write 'src/**/*.(sol|json)' 'script/*.sol'",
"format:check": "prettier --check 'src/**/*.*(sol|json)' 'script/*.sol'",
"lint": "solhint 'src/**/*.sol' 'script/*.sol'",
"lint:fix": "solhint --fix 'src/**/*.sol' 'script/*.sol'"
}
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
Loading
Loading