Skip to content

Commit

Permalink
Feat/tests unit and fuzz (#5)
Browse files Browse the repository at this point in the history
* feat: extra checks inside constructor

* chore: remove useless gitkeeps

* feat: handle rounding down of totalAssets

* doc: add spe cification of assets being vested

* feat: give allowance

* feat: DeployERC4626Strategy script

* feat: replace WAD to BPS

* fix: deploy strategy script

* chore: add utils dependency

* tests: unit tests

* tests: fuzz tests

* fix: compilers warnings

* chore: setup repo in ci

* chore: add ethereum uri to coverage

* fix: swap fuzz test now checks vesting

* fix: correct bps value

* refactor: rename Normal to Success

* fix: correct fees according to BPS

* fix: fees tests are correct

* tests: improve tests with more robust

* tests: numerical examples
  • Loading branch information
0xtekgrinder authored Aug 3, 2024
1 parent c2cf40c commit 535bf2a
Show file tree
Hide file tree
Showing 41 changed files with 1,776 additions and 16 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/ci-deep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ jobs:
with:
version: nightly

- name: Setup repo
uses: ./.github/actions/setup-repo
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Run Foundry tests
run: yarn test:unit
env:
Expand Down Expand Up @@ -123,6 +128,11 @@ jobs:
with:
version: nightly

- name: Setup repo
uses: ./.github/actions/setup-repo
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Run Foundry tests
run: yarn test:invariant
env:
Expand Down Expand Up @@ -159,6 +169,11 @@ jobs:
with:
version: nightly

- name: Setup repo
uses: ./.github/actions/setup-repo
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Run Foundry tests
run: yarn test:fuzz
env:
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ jobs:
out
key: "build-${{ github.sha }}"

- name: Setup repo
uses: ./.github/actions/setup-repo
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
Expand Down Expand Up @@ -112,6 +117,11 @@ jobs:
with:
version: nightly

- name: Setup repo
uses: ./.github/actions/setup-repo
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Run Foundry tests
run: yarn test:invariant
env:
Expand Down Expand Up @@ -143,6 +153,11 @@ jobs:
out
key: "build-${{ github.sha }}"

- name: Setup repo
uses: ./.github/actions/setup-repo
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
Expand Down Expand Up @@ -173,11 +188,18 @@ jobs:
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"

- name: Setup repo
uses: ./.github/actions/setup-repo
with:
registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }}

- name: "Install lcov"
run: "sudo apt-get install lcov"

- name: "Generate the coverage report using the unit and the integration tests"
run: "yarn ci:coverage"
env:
ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }}

- name: "Upload coverage report to Codecov"
uses: "codecov/codecov-action@v3"
Expand Down
30 changes: 18 additions & 12 deletions contracts/BaseStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
bytes32 public constant INTEGRATOR_ROLE = keccak256("INTEGRATOR_ROLE");
bytes32 public constant DEVELOPER_ROLE = keccak256("DEVELOPER_ROLE");

uint32 public constant WAD = 100_000; // 100%
uint32 public constant MAX_FEE = 50_000; // 50%
uint32 public constant BPS = 10_000; // 100%
uint32 public constant MAX_FEE = 5_000; // 50%

/**
* @notice The offset to convert the decimals
Expand Down Expand Up @@ -162,14 +162,17 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
constructor(
ConstructorArgs memory args
) ERC20(args.definitiveName, args.definitiveSymbol) ERC4626(IERC20(args.definitiveAsset)) {
if (args.initialPerformanceFee > WAD || args.initialDeveloperFee > WAD || args.initialDeveloperFee > MAX_FEE) {
if (args.initialPerformanceFee > BPS || args.initialDeveloperFee > MAX_FEE) {
revert InvalidFee();
}
if (
args.initialIntegratorFeeRecipient == address(0) ||
args.initialDeveloperFeeRecipient == address(0) ||
args.initialSwapRouter == address(0) ||
args.initialTokenTransferAddress == address(0) ||
args.initialKeeper == address(0) ||
args.initialDeveloper == address(0) ||
args.initialIntegrator == address(0) ||
args.definitiveStrategyAsset == address(0)
) {
revert ZeroAddress();
Expand All @@ -186,6 +189,9 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
STRATEGY_ASSET = args.definitiveStrategyAsset;
_DECIMALS_OFFSET = uint8(uint256(18).zeroFloorSub(decimals()));

// give allowance
IERC20(args.definitiveAsset).safeIncreaseAllowance(args.definitiveStrategyAsset, type(uint256).max);

// Roles managment
_grantRole(KEEPER_ROLE, args.initialKeeper);
_grantRole(DEVELOPER_ROLE, args.initialDeveloper);
Expand All @@ -210,7 +216,7 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
* @inheritdoc ERC4626
*/
function totalAssets() public view override returns (uint256) {
return _assetsHeld() - lockedProfit();
return _assetsHeld().zeroFloorSub(lockedProfit()); // handle rounding down of assets
}

/**
Expand Down Expand Up @@ -459,8 +465,8 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
newTotalAssets = totalAssets();

// `newTotalAssets.zeroFloorSub(lastTotalAssets)` is the value of the total interest earned by the strategy.
// `feeAssets` may be rounded down to 0 if `totalInterest * fee < WAD`.
uint256 feeAssets = (newTotalAssets.zeroFloorSub(lastTotalAssets)).mulDiv(performanceFee, WAD);
// `feeAssets` may be rounded down to 0 if `totalInterest * fee < BPS`.
uint256 feeAssets = (newTotalAssets.zeroFloorSub(lastTotalAssets)).mulDiv(performanceFee, BPS);
if (feeAssets != 0) {
// The fee assets is subtracted from the total assets in these calculations to compensate for the fact
// that total assets is already increased by the total interest (including the fee assets).
Expand All @@ -471,8 +477,8 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
Math.Rounding.Floor
);

developerFeeShares = feeShares.mulDiv(developerFee, WAD);
integratorFeeShares = feeShares - developerFeeShares; // Cannot underflow as developerFee <= WAD
developerFeeShares = feeShares.mulDiv(developerFee, BPS);
integratorFeeShares = feeShares - developerFeeShares; // Cannot underflow as developerFee <= BPS
}
}

Expand All @@ -486,8 +492,6 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
* @custom:requires INTEGRATOR_ROLE
*/
function setVestingPeriod(uint64 newVestingPeriod) external onlyRole(INTEGRATOR_ROLE) {
if (newVestingPeriod == 0 || newVestingPeriod > 365 days) revert InvalidParameter();

vestingPeriod = newVestingPeriod;

emit VestingPeriodUpdated(newVestingPeriod);
Expand All @@ -499,7 +503,7 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
* @custom:requires INTEGRATOR_ROLE
*/
function setPerformanceFee(uint32 newPerformanceFee) external onlyRole(INTEGRATOR_ROLE) {
if (newPerformanceFee > WAD) {
if (newPerformanceFee > BPS) {
revert InvalidFee();
}
performanceFee = newPerformanceFee;
Expand All @@ -513,7 +517,7 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
* @custom:requires DEVELOPER_ROLE
*/
function setDeveloperFee(uint32 newDeveloperFee) external onlyRole(DEVELOPER_ROLE) {
if (newDeveloperFee > WAD || newDeveloperFee > performanceFee) {
if (newDeveloperFee > MAX_FEE) {
revert InvalidFee();
}
developerFee = newDeveloperFee;
Expand Down Expand Up @@ -581,6 +585,8 @@ abstract contract BaseStrategy is ERC4626, AccessControl {
* @param callDatas array of bytes to call the router/aggregator
* @param amounts array of amounts to swap
* @custom:requires KEEPER_ROLE
*
* @dev the assets which are getting vested yield rewards for the whole streategy
*/
function swap(
address[] calldata tokens,
Expand Down
4 changes: 2 additions & 2 deletions contracts/ERC4626Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ contract ERC4626Strategy is BaseStrategy {
/**
* @inheritdoc ERC4626
*/
function maxDeposit(address user) public view override returns (uint256) {
function maxDeposit(address) public view override returns (uint256) {
return ERC4626(STRATEGY_ASSET).maxDeposit(address(this));
}

/**
* @inheritdoc ERC4626
* @dev might overflow (in this case, the user should be able to mint the type(uint256).max)
*/
function maxMint(address user) public view override returns (uint256) {
function maxMint(address) public view override returns (uint256) {
return _convertToShares(ERC4626(STRATEGY_ASSET).maxDeposit(address(this)), Math.Rounding.Floor);
}

Expand Down
Empty file removed contracts/example/.gitkeep
Empty file.
Empty file removed contracts/external/.gitkeep
Empty file.
2 changes: 1 addition & 1 deletion lib/utils
Submodule utils updated 1 files
+1 −0 src/CommonUtils.sol
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
oz/=lib/openzeppelin-contracts/contracts/
oz-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
morpho/=lib/morpho-blue/src/
morpho/=lib/morpho-blue/src/
utils/=lib/utils/
Empty file removed scripts/.gitkeep
Empty file.
56 changes: 56 additions & 0 deletions scripts/DeployER4626Strategy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import { ERC4626Strategy, BaseStrategy } from "../contracts/ERC4626Strategy.sol";
import "utils/src/CommonUtils.sol";
import "../test/Constants.t.sol";
import "forge-std/Script.sol";

contract DeployERC4626Strategy is Script, CommonUtils {
function run() external {
uint256 chainId = CHAIN_SOURCE;
uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC_MAINNET"), "m/44'/60'/0'/0/", 0);

vm.startBroadcast(deployerPrivateKey);

address deployer = vm.addr(deployerPrivateKey);
console.log("Deployer address: ", deployer);

/** TODO complete */
address asset = _chainToContract(chainId, ContractType.AgUSD);
address strategyAsset = _chainToContract(chainId, ContractType.StUSD);

address integrator = _chainToContract(chainId, ContractType.GuardianMultisig);
address developer = _chainToContract(chainId, ContractType.GuardianMultisig);
address keeper = 0xa9bbbDDe822789F123667044443dc7001fb43C01;

uint32 performanceFee = 1_000; // 10%
uint32 developerFee = 2_000; // 20%

string memory name = "stUSD Strategy";
string memory symbol = "stUSDStrat";
/** END complete */

ERC4626Strategy strategy = new ERC4626Strategy(
BaseStrategy.ConstructorArgs(
performanceFee,
developerFee,
integrator,
developer,
keeper,
developer,
integrator,
ONEINCH_ROUTER,
ONEINCH_ROUTER,
1 weeks,
name,
symbol,
asset,
strategyAsset
)
);
console.log("Strategy address: ", address(strategy));

vm.stopBroadcast();
}
}
23 changes: 23 additions & 0 deletions test/BaseTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: Unlicensed
pragma solidity 0.8.26;

import "forge-std/Test.sol";
import "utils/src/CommonUtils.sol";
import { IERC20 } from "forge-std/interfaces/IERC20.sol";

contract BaseTest is Test, CommonUtils {
// Useful addresses
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
address public integrator = makeAddr("integrator");
address public keeper = makeAddr("keeper");
address public developer = makeAddr("developer");

function setUp() public virtual {
vm.label(alice, "alice");
vm.label(bob, "bob");
vm.label(integrator, "integrator");
vm.label(keeper, "keeper");
vm.label(developer, "developer");
}
}
6 changes: 6 additions & 0 deletions test/Constants.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Unlicensed
pragma solidity 0.8.26;

uint256 constant CHAIN_SOURCE = 1;
address constant ONEINCH_ROUTER = 0x111111125421cA6dc452d289314280a0f8842A65;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
42 changes: 42 additions & 0 deletions test/ERC4626StrategyTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.26;

import "./BaseTest.t.sol";
import "./Constants.t.sol";
import "../contracts/utils/Errors.sol";
import { IAccessControl } from "oz/access/AccessControl.sol";
import { ERC4626Strategy, BaseStrategy, ERC4626 } from "../contracts/ERC4626Strategy.sol";

contract ERC4626StrategyTest is BaseTest {
ERC4626Strategy public strategy;
address public asset;
address public strategyAsset;

function setUp() public virtual override {
super.setUp();

vm.createSelectFork("mainnet", 20363172);

asset = _chainToContract(CHAIN_SOURCE, ContractType.AgUSD);
strategyAsset = _chainToContract(CHAIN_SOURCE, ContractType.StUSD);

strategy = new ERC4626Strategy(
BaseStrategy.ConstructorArgs(
1_000, // 10%
2_000, // 20%
integrator,
developer,
keeper,
developer,
integrator,
ONEINCH_ROUTER,
ONEINCH_ROUTER,
1 weeks,
"stUSD Strategy",
"stUSDStrat",
asset,
strategyAsset
)
);
}
}
Empty file removed test/fuzz/.gitkeep
Empty file.
Loading

0 comments on commit 535bf2a

Please sign in to comment.