Skip to content

Commit

Permalink
feat: v3.0.3 (#98)
Browse files Browse the repository at this point in the history
* chore: bump api version

* test: fix invariant test

* feat: max deposit (#99)

* feat: max uint deposit

* fix: invariant test

* fix: rebase

* fix: dont max mint

* fix: uint 256

Co-authored-by: FP <[email protected]>

* fix: uint 256

Co-authored-by: FP <[email protected]>

* test: check balances

---------

Co-authored-by: FP <[email protected]>

* chore: add statemind audit

* chore: match abis (#100)

* feat: update name (#101)

* chore: update domain separator (#102)

* chore: update domain seperator

* feat: dont cache domain

* chore: test runs

* fix: comment

Co-authored-by: spalen0 <[email protected]>

* fix: comment

Co-authored-by: spalen0 <[email protected]>

* fix: permit cast (#104)

* fix: deposit flow (#103)

* fix: conversion

* test: full loss

* feat: add self check to max views

* test: balance check

* fix: spelling

* chore: deployed

---------

Co-authored-by: FP <[email protected]>
Co-authored-by: spalen0 <[email protected]>
  • Loading branch information
3 people authored Sep 24, 2024
1 parent 4321cbc commit e90ecb8
Show file tree
Hide file tree
Showing 15 changed files with 5,211 additions and 83 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Deployments on new chains can be done permissionlessly by anyone using the inclu
You can then deploy the TokenizedStrategy using the provided script.

```
forge script script/Deploy.s.sol:Deploy --rpc-url YOUR_RPC_URL
forge script script/Deploy.s.sol:Deploy --account ACCOUNT_NAME --rpc-url YOUR_RPC_URL --broadcast
```

If the deployments do not end at the same address you can also manually send the calldata used in the previous deployments on other chains.
Expand Down
Binary file added audits/Yearn V3 report Statemind.pdf
Binary file not shown.
1,489 changes: 1,489 additions & 0 deletions flattened/FlatBaseStrategy.sol

Large diffs are not rendered by default.

3,378 changes: 3,378 additions & 0 deletions flattened/FlatTokenizedStrategy.sol

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ src = 'src'
out = 'out'
libs = ['lib']
solc = "0.8.18"
evm_version = "paris"

remappings = [
'forge-std/=lib/forge-std/src/',
Expand All @@ -17,5 +18,3 @@ max_test_rejects = 1_000_000
[invariant]
runs = 100
depth = 100

# See more config options https://github.com/gakonst/foundry/tree/master/config
9 changes: 4 additions & 5 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ contract Deploy is Script {
Deployer public deployer =
Deployer(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed);

// Vault factory address for v3.0.2
address public factory = 0x444045c5C13C246e117eD36437303cac8E250aB0;
// Vault factory address for v3.0.3
address public factory = 0x5577EdcB8A856582297CdBbB07055E6a6E38eb5f;

function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
vm.startBroadcast();

// Append constructor args to the bytecode
bytes memory bytecode = abi.encodePacked(
Expand All @@ -23,7 +22,7 @@ contract Deploy is Script {
);

// Pick an unique salt
bytes32 salt = keccak256("v3.0.2");
bytes32 salt = keccak256("v3.0.3");

address contractAddress = deployer.deployCreate2(salt, bytecode);

Expand Down
122 changes: 65 additions & 57 deletions src/TokenizedStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,8 @@ contract TokenizedStrategy {
// These are the corresponding ERC20 variables needed for the
// strategies token that is issued and burned on each deposit or withdraw.
uint8 decimals; // The amount of decimals that `asset` and strategy use.
uint88 INITIAL_CHAIN_ID; // The initial chain id when the strategy was created.
string name; // The name of the token for the strategy.
uint256 totalSupply; // The total amount of shares currently issued.
bytes32 INITIAL_DOMAIN_SEPARATOR; // The domain separator used for permits on the initial chain.
mapping(address => uint256) nonces; // Mapping of nonces used for permit functions.
mapping(address => uint256) balances; // Mapping to track current balances for each account that holds shares.
mapping(address => mapping(address => uint256)) allowances; // Mapping to track the allowances for the strategies shares.
Expand Down Expand Up @@ -345,7 +343,7 @@ contract TokenizedStrategy {
//////////////////////////////////////////////////////////////*/

/// @notice API version this TokenizedStrategy implements.
string internal constant API_VERSION = "3.0.2";
string internal constant API_VERSION = "3.0.3";

/// @notice Value to set the `entered` flag to during a call.
uint8 internal constant ENTERED = 2;
Expand Down Expand Up @@ -451,11 +449,6 @@ contract TokenizedStrategy {
S.name = _name;
// Set decimals based off the `asset`.
S.decimals = ERC20(_asset).decimals();
// Set initial chain id for permit replay protection.
require(block.chainid < type(uint88).max, "invalid chain id");
S.INITIAL_CHAIN_ID = uint88(block.chainid);
// Set the initial domain separator for permit functions.
S.INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator(S);

// Default to a 10 day profit unlock period.
S.profitMaxUnlockTime = 10 days;
Expand Down Expand Up @@ -497,6 +490,12 @@ contract TokenizedStrategy {
) external nonReentrant returns (uint256 shares) {
// Get the storage slot for all following calls.
StrategyData storage S = _strategyStorage();

// Deposit full balance if using max uint.
if (assets == type(uint256).max) {
assets = S.asset.balanceOf(msg.sender);
}

// Checking max deposit will also check if shutdown.
require(
assets <= _maxDeposit(S, receiver),
Expand Down Expand Up @@ -524,6 +523,7 @@ contract TokenizedStrategy {
) external nonReentrant returns (uint256 assets) {
// Get the storage slot for all following calls.
StrategyData storage S = _strategyStorage();

// Checking max mint will also check if shutdown.
require(shares <= _maxMint(S, receiver), "ERC4626: mint more than max");
// Check for rounding error.
Expand Down Expand Up @@ -746,26 +746,25 @@ contract TokenizedStrategy {

/**
* @notice Total number of underlying assets that can
* be deposited by `_owner` into the strategy, where `owner`
* corresponds to the receiver of a {deposit} call.
* be deposited into the strategy, where `receiver`
* corresponds to the receiver of the shares of a {deposit} call.
*
* @param owner The address depositing.
* @return . The max that `owner` can deposit in `asset`.
* @param receiver The address receiving the shares.
* @return . The max that `receiver` can deposit in `asset`.
*/
function maxDeposit(address owner) external view returns (uint256) {
return _maxDeposit(_strategyStorage(), owner);
function maxDeposit(address receiver) external view returns (uint256) {
return _maxDeposit(_strategyStorage(), receiver);
}

/**
* @notice Total number of shares that can be minted by `owner`
* into the strategy, where `_owner` corresponds to the receiver
* @notice Total number of shares that can be minted to `receiver`
* of a {mint} call.
*
* @param owner The address minting.
* @return _maxMint The max that `owner` can mint in shares.
* @param receiver The address receiving the shares.
* @return _maxMint The max that `receiver` can mint in shares.
*/
function maxMint(address owner) external view returns (uint256) {
return _maxMint(_strategyStorage(), owner);
function maxMint(address receiver) external view returns (uint256) {
return _maxMint(_strategyStorage(), receiver);
}

/**
Expand All @@ -780,6 +779,18 @@ contract TokenizedStrategy {
return _maxWithdraw(_strategyStorage(), owner);
}

/**
* @notice Variable `maxLoss` is ignored.
* @dev Accepts a `maxLoss` variable in order to match the multi
* strategy vaults ABI.
*/
function maxWithdraw(
address owner,
uint256 /*maxLoss*/
) external view returns (uint256) {
return _maxWithdraw(_strategyStorage(), owner);
}

/**
* @notice Total number of strategy shares that can be
* redeemed from the strategy by `owner`, where `owner`
Expand All @@ -792,6 +803,18 @@ contract TokenizedStrategy {
return _maxRedeem(_strategyStorage(), owner);
}

/**
* @notice Variable `maxLoss` is ignored.
* @dev Accepts a `maxLoss` variable in order to match the multi
* strategy vaults ABI.
*/
function maxRedeem(
address owner,
uint256 /*maxLoss*/
) external view returns (uint256) {
return _maxRedeem(_strategyStorage(), owner);
}

/*//////////////////////////////////////////////////////////////
INTERNAL 4626 VIEW METHODS
//////////////////////////////////////////////////////////////*/
Expand All @@ -816,12 +839,14 @@ contract TokenizedStrategy {
uint256 assets,
Math.Rounding _rounding
) internal view returns (uint256) {
// Saves an extra SLOAD if totalAssets() is non-zero.
uint256 totalAssets_ = _totalAssets(S);
// Saves an extra SLOAD if values are non-zero.
uint256 totalSupply_ = _totalSupply(S);
// If supply is 0, PPS = 1.
if (totalSupply_ == 0) return assets;

uint256 totalAssets_ = _totalAssets(S);
// If assets are 0 but supply is not PPS = 0.
if (totalAssets_ == 0) return totalSupply_ == 0 ? assets : 0;
if (totalAssets_ == 0) return 0;

return assets.mulDiv(totalSupply_, totalAssets_, _rounding);
}
Expand All @@ -844,23 +869,23 @@ contract TokenizedStrategy {
/// @dev Internal implementation of {maxDeposit}.
function _maxDeposit(
StrategyData storage S,
address owner
address receiver
) internal view returns (uint256) {
// Cannot deposit when shutdown.
if (S.shutdown) return 0;
// Cannot deposit when shutdown or to the strategy.
if (S.shutdown || receiver == address(this)) return 0;

return IBaseStrategy(address(this)).availableDepositLimit(owner);
return IBaseStrategy(address(this)).availableDepositLimit(receiver);
}

/// @dev Internal implementation of {maxMint}.
function _maxMint(
StrategyData storage S,
address owner
address receiver
) internal view returns (uint256 maxMint_) {
// Cannot mint when shutdown.
if (S.shutdown) return 0;
// Cannot mint when shutdown or to the strategy.
if (S.shutdown || receiver == address(this)) return 0;

maxMint_ = IBaseStrategy(address(this)).availableDepositLimit(owner);
maxMint_ = IBaseStrategy(address(this)).availableDepositLimit(receiver);
if (maxMint_ != type(uint256).max) {
maxMint_ = _convertToShares(S, maxMint_, Math.Rounding.Down);
}
Expand Down Expand Up @@ -932,8 +957,6 @@ contract TokenizedStrategy {
uint256 assets,
uint256 shares
) internal {
require(receiver != address(this), "ERC4626: mint to self");

// Cache storage variables used more than once.
ERC20 _asset = S.asset;

Expand Down Expand Up @@ -1594,6 +1617,14 @@ contract TokenizedStrategy {
emit UpdateProfitMaxUnlockTime(_profitMaxUnlockTime);
}

/**
* @notice Updates the name for the strategy.
* @param _name The new name for the strategy.
*/
function setName(string calldata _name) external onlyManagement {
_strategyStorage().name = _name;
}

/*//////////////////////////////////////////////////////////////
ERC20 METHODS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -1982,39 +2013,16 @@ contract TokenizedStrategy {
* @notice Returns the domain separator used in the encoding of the signature
* for {permit}, as defined by {EIP712}.
*
* @dev This checks that the current chain id is the same as when the contract
* was deployed to prevent replay attacks. If false it will calculate a new
* domain separator based on the new chain id.
*
* @return . The domain separator that will be used for any {permit} calls.
*/
function DOMAIN_SEPARATOR() public view returns (bytes32) {
StrategyData storage S = _strategyStorage();
return
block.chainid == S.INITIAL_CHAIN_ID
? S.INITIAL_DOMAIN_SEPARATOR
: _computeDomainSeparator(S);
}

/**
* @dev Calculates and returns the domain separator to be used in any
* permit functions for the strategies {permit} calls.
*
* This will be used at the initialization of each new strategies storage.
* It would then be used in the future in the case of any forks in which
* the current chain id is not the same as the original.
*
*/
function _computeDomainSeparator(
StrategyData storage S
) internal view returns (bytes32) {
return
keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes(S.name)),
keccak256("Yearn Vault"),
keccak256(bytes(API_VERSION)),
block.chainid,
address(this)
Expand Down
12 changes: 12 additions & 0 deletions src/interfaces/ITokenizedStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ interface ITokenizedStrategy is IERC4626, IERC20Permit {
uint256 maxLoss
) external returns (uint256);

function maxWithdraw(
address owner,
uint256 /*maxLoss*/
) external view returns (uint256);

function maxRedeem(
address owner,
uint256 /*maxLoss*/
) external view returns (uint256);

/*//////////////////////////////////////////////////////////////
MODIFIER HELPERS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -150,6 +160,8 @@ interface ITokenizedStrategy is IERC4626, IERC20Permit {

function setProfitMaxUnlockTime(uint256 _profitMaxUnlockTime) external;

function setName(string calldata _newName) external;

function shutdownStrategy() external;

function emergencyWithdraw(uint256 _amount) external;
Expand Down
15 changes: 15 additions & 0 deletions src/test/AccessControl.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,19 @@ contract AccessControlTest is Setup {
vm.prank(keeper);
strategy.tend();
}

function test_setName(address _address) public {
vm.assume(_address != address(strategy) && _address != management);

string memory newName = "New Strategy Name";

vm.prank(_address);
vm.expectRevert("!management");
strategy.setName(newName);

vm.prank(management);
strategy.setName(newName);

assertEq(strategy.name(), newName);
}
}
Loading

0 comments on commit e90ecb8

Please sign in to comment.