diff --git a/.gitmodules b/.gitmodules index c82aa28..ae9d5ae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/aave-v3-core"] - path = lib/aave-v3-core - url = https://github.com/aave/aave-v3-core [submodule "lib/aave-address-book"] path = lib/aave-address-book url = https://github.com/bgd-labs/aave-address-book diff --git a/foundry.toml b/foundry.toml index 25b918f..11c911f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,7 @@ src = "src" out = "out" libs = ["lib"] +solc = "0.8.24" +evm_version = "cancun" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/aave-address-book b/lib/aave-address-book index 9d7dd12..fd4e3ab 160000 --- a/lib/aave-address-book +++ b/lib/aave-address-book @@ -1 +1 @@ -Subproject commit 9d7dd12291d140c444b9588ef6ced8f860f6b5c0 +Subproject commit fd4e3abfd238e1a1ab77d34e979d4229aae61290 diff --git a/lib/aave-v3-core b/lib/aave-v3-core deleted file mode 160000 index 6070e82..0000000 --- a/lib/aave-v3-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6070e82d962d9b12835c88e68210d0e63f08d035 diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..0e89b05 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,8 @@ +aave-v3-core/=lib/aave-address-book/lib/aave-v3-origin/src/core/ +aave-v3-periphery/=lib/aave-address-book/lib/aave-v3-origin/src/periphery/ +aave-address-book/=lib/aave-address-book/src/ +aave-v3-origin/=lib/aave-address-book/lib/aave-v3-origin/src/ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +solidity-utils/=lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/src/ +openzeppelin/=lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts \ No newline at end of file diff --git a/src/BaseTokenWrapper.sol b/src/BaseTokenWrapper.sol index 8da9901..209a8c6 100644 --- a/src/BaseTokenWrapper.sol +++ b/src/BaseTokenWrapper.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.10; import {Ownable} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/Ownable.sol'; import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {IERC20WithPermit} from 'aave-v3-core/contracts/interfaces/IERC20WithPermit.sol'; +import {SafeERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/SafeERC20.sol'; import {GPv2SafeERC20} from 'aave-v3-core/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; import {IAToken} from 'aave-v3-core/contracts/interfaces/IAToken.sol'; +import {ICreditDelegationToken} from 'aave-v3-core/contracts/interfaces/ICreditDelegationToken.sol'; import {IBaseTokenWrapper} from './interfaces/IBaseTokenWrapper.sol'; /** @@ -26,6 +28,8 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { /// @inheritdoc IBaseTokenWrapper IPool public immutable POOL; + uint256 private constant VARIABLE_INTEREST_RATE_MODE = 2; + /** * @dev Constructor * @param tokenIn ERC-20 token that will be wrapped in supply operations @@ -38,7 +42,6 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { TOKEN_OUT = tokenOut; POOL = IPool(pool); transferOwnership(owner); - IERC20(tokenOut).approve(pool, type(uint256).max); } /// @inheritdoc IBaseTokenWrapper @@ -46,7 +49,7 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { uint256 amount, address onBehalfOf, uint16 referralCode - ) external returns (uint256) { + ) external virtual returns (uint256) { return _supplyToken(amount, onBehalfOf, referralCode); } @@ -56,7 +59,7 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { address onBehalfOf, uint16 referralCode, PermitSignature calldata signature - ) external returns (uint256) { + ) external virtual returns (uint256) { // explicitly left try-catch block blank to protect users from permit griefing try IERC20WithPermit(TOKEN_IN).permit( @@ -76,7 +79,7 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { function withdrawToken( uint256 amount, address to - ) external returns (uint256) { + ) external virtual returns (uint256) { IAToken aTokenOut = IAToken(POOL.getReserveData(TOKEN_OUT).aTokenAddress); return _withdrawToken(amount, to, aTokenOut); } @@ -86,7 +89,7 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { uint256 amount, address to, PermitSignature calldata signature - ) external returns (uint256) { + ) external virtual returns (uint256) { IAToken aTokenOut = IAToken(POOL.getReserveData(TOKEN_OUT).aTokenAddress); // explicitly left try-catch block blank to protect users from permit griefing try @@ -103,6 +106,37 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { return _withdrawToken(amount, to, aTokenOut); } + /// @inheritdoc IBaseTokenWrapper + function borrowToken(uint256 amount, uint16 referralCode) external virtual { + _borrowToken(amount, msg.sender, referralCode); + } + + /// @inheritdoc IBaseTokenWrapper + function borrowTokenWithPermit( + uint256 amount, + uint16 referralCode, + PermitSignature calldata signature + ) external virtual { + if (signature.deadline != 0) { + address debtToken = POOL + .getReserveData(TOKEN_OUT) + .variableDebtTokenAddress; + // explicitly left try-catch block blank to protect users from permit griefing + try + ICreditDelegationToken(debtToken).delegationWithSig( + msg.sender, + address(this), + amount, + signature.deadline, + signature.v, + signature.r, + signature.s + ) + {} catch {} + } + _borrowToken(amount, msg.sender, referralCode); + } + /// @inheritdoc IBaseTokenWrapper function rescueTokens( IERC20 token, @@ -144,7 +178,9 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { IERC20(TOKEN_IN).safeTransferFrom(msg.sender, address(this), amount); uint256 amountWrapped = _wrapTokenIn(amount); require(amountWrapped > 0, 'INSUFFICIENT_WRAPPED_TOKEN_RECEIVED'); + SafeERC20.safeApprove(IERC20(TOKEN_OUT), address(POOL), amountWrapped); POOL.supply(TOKEN_OUT, amountWrapped, onBehalfOf, referralCode); + SafeERC20.safeApprove(IERC20(TOKEN_OUT), address(POOL), 0); return amountWrapped; } @@ -177,6 +213,33 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper { return amountUnwrapped; } + /** + * @notice Helper to borrow token from the Pool and unwraps it, sending to the recipient + * @param amount The amount of token to borrow + * @param onBehalfOf The address that will receive the unwrapped token + * @param referralCode Code used to register the integrator originating the operation, for potential rewards + */ + function _borrowToken( + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) internal { + require(amount > 0, 'INSUFFICIENT_AMOUNT_TO_BORROW'); + uint256 balanceBeforeBorrow = IERC20(TOKEN_OUT).balanceOf(address(this)); + POOL.borrow( + TOKEN_OUT, + amount, + VARIABLE_INTEREST_RATE_MODE, + referralCode, + address(onBehalfOf) + ); + uint256 balanceAfterBorrow = IERC20(TOKEN_OUT).balanceOf(address(this)); + uint256 amountIn = _unwrapTokenOut( + balanceAfterBorrow - balanceBeforeBorrow + ); + IERC20(TOKEN_IN).transfer(onBehalfOf, amountIn); + } + /** * @notice Helper to wrap an amount of tokenIn, receiving tokenOut * @param amount The amount of tokenIn to wrap diff --git a/src/Generic4626Wrapper.sol b/src/Generic4626Wrapper.sol new file mode 100644 index 0000000..ea2257a --- /dev/null +++ b/src/Generic4626Wrapper.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {SafeERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/SafeERC20.sol'; +import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC4626} from 'openzeppelin/interfaces/IERC4626.sol'; +import {BaseTokenWrapper} from './BaseTokenWrapper.sol'; + +/** + * @title Generic4626Wrapper + * @author Aave + * @notice Generic contract to wrap an ERC20 to ERC4626 to on supply to Aave, or unwrap from ERC4626 to ERC20 on withdrawal + */ +contract Generic4626Wrapper is BaseTokenWrapper { + /** + * @dev Constructor + * @param tokenIn Address for the ERC20 token + * @param tokenOut Address for the ERC4626 token + * @param pool The address of the Aave Pool + * @param owner The address to transfer ownership to + */ + constructor( + address tokenIn, + address tokenOut, + address pool, + address owner + ) BaseTokenWrapper(tokenIn, tokenOut, pool, owner) { + // Intentionally left blank + } + + /// @inheritdoc BaseTokenWrapper + function getTokenOutForTokenIn( + uint256 amount + ) external view override returns (uint256) { + return IERC4626(TOKEN_OUT).previewDeposit(amount); + } + + /// @inheritdoc BaseTokenWrapper + function getTokenInForTokenOut( + uint256 amount + ) external view override returns (uint256) { + return IERC4626(TOKEN_OUT).previewRedeem(amount); + } + + /// @inheritdoc BaseTokenWrapper + function _wrapTokenIn(uint256 amount) internal override returns (uint256) { + SafeERC20.safeApprove(IERC20(TOKEN_IN), TOKEN_OUT, amount); + uint256 wrappedAmount = IERC4626(TOKEN_OUT).deposit(amount, address(this)); + SafeERC20.safeApprove(IERC20(TOKEN_IN), TOKEN_OUT, 0); + return wrappedAmount; + } + + /// @inheritdoc BaseTokenWrapper + function _unwrapTokenOut(uint256 amount) internal override returns (uint256) { + return IERC4626(TOKEN_OUT).redeem(amount, address(this), address(this)); + } +} diff --git a/src/SavingsDaiTokenWrapper.sol b/src/SavingsDaiTokenWrapper.sol index 4c07ea1..b136645 100644 --- a/src/SavingsDaiTokenWrapper.sol +++ b/src/SavingsDaiTokenWrapper.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.10; +import {SafeERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/SafeERC20.sol'; import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; -import {ISavingsDai} from './interfaces/ISavingsDai.sol'; +import {ISavingsDai} from './dependencies/ISavingsDai.sol'; import {BaseTokenWrapper} from './BaseTokenWrapper.sol'; /** @@ -24,7 +25,31 @@ contract SavingsDaiTokenWrapper is BaseTokenWrapper { address pool, address owner ) BaseTokenWrapper(tokenIn, tokenOut, pool, owner) { - IERC20(tokenIn).approve(tokenOut, type(uint256).max); + // Intentionally left blank + } + + /// @inheritdoc BaseTokenWrapper + function supplyTokenWithPermit( + uint256, + address, + uint16, + PermitSignature calldata + ) external pure override returns (uint256) { + revert('INVALID_ACTION'); + } + + /// @inheritdoc BaseTokenWrapper + function borrowToken(uint256, uint16) external pure override { + revert('INVALID_ACTION'); + } + + /// @inheritdoc BaseTokenWrapper + function borrowTokenWithPermit( + uint256, + uint16, + PermitSignature calldata + ) external pure override { + revert('INVALID_ACTION'); } /// @inheritdoc BaseTokenWrapper @@ -43,7 +68,13 @@ contract SavingsDaiTokenWrapper is BaseTokenWrapper { /// @inheritdoc BaseTokenWrapper function _wrapTokenIn(uint256 amount) internal override returns (uint256) { - return ISavingsDai(TOKEN_OUT).deposit(amount, address(this)); + SafeERC20.safeApprove(IERC20(TOKEN_IN), TOKEN_OUT, amount); + uint256 wrappedAmount = ISavingsDai(TOKEN_OUT).deposit( + amount, + address(this) + ); + SafeERC20.safeApprove(IERC20(TOKEN_IN), TOKEN_OUT, 0); + return wrappedAmount; } /// @inheritdoc BaseTokenWrapper diff --git a/src/StakedEthTokenWrapper.sol b/src/StakedEthTokenWrapper.sol index 58c469c..ab58a39 100644 --- a/src/StakedEthTokenWrapper.sol +++ b/src/StakedEthTokenWrapper.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.10; +import {SafeERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/SafeERC20.sol'; import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; -import {IWstETH} from './interfaces/IWstETH.sol'; +import {IWstETH} from './dependencies/IWstETH.sol'; import {BaseTokenWrapper} from './BaseTokenWrapper.sol'; /** @@ -24,7 +25,21 @@ contract StakedEthTokenWrapper is BaseTokenWrapper { address pool, address owner ) BaseTokenWrapper(tokenIn, tokenOut, pool, owner) { - IERC20(tokenIn).approve(tokenOut, type(uint256).max); + // Intentionally left blank + } + + /// @inheritdoc BaseTokenWrapper + function borrowToken(uint256, uint16) external pure override { + revert('INVALID_ACTION'); + } + + /// @inheritdoc BaseTokenWrapper + function borrowTokenWithPermit( + uint256, + uint16, + PermitSignature calldata + ) external pure override { + revert('INVALID_ACTION'); } /// @inheritdoc BaseTokenWrapper @@ -43,7 +58,10 @@ contract StakedEthTokenWrapper is BaseTokenWrapper { /// @inheritdoc BaseTokenWrapper function _wrapTokenIn(uint256 amount) internal override returns (uint256) { - return IWstETH(TOKEN_OUT).wrap(amount); + SafeERC20.safeApprove(IERC20(TOKEN_IN), TOKEN_OUT, amount); + uint256 wrappedAmount = IWstETH(TOKEN_OUT).wrap(amount); + SafeERC20.safeApprove(IERC20(TOKEN_IN), TOKEN_OUT, 0); + return wrappedAmount; } /// @inheritdoc BaseTokenWrapper diff --git a/src/interfaces/ISavingsDai.sol b/src/dependencies/ISavingsDai.sol similarity index 100% rename from src/interfaces/ISavingsDai.sol rename to src/dependencies/ISavingsDai.sol diff --git a/src/dependencies/IUSDS.sol b/src/dependencies/IUSDS.sol new file mode 100644 index 0000000..a4e1dd7 --- /dev/null +++ b/src/dependencies/IUSDS.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity >=0.8.0; + +interface IUSDS { + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function PERMIT_TYPEHASH() external view returns (bytes32); + + function UPGRADE_INTERFACE_VERSION() external view returns (string memory); + + function allowance( + address owner, + address spender + ) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function asset() external view returns (address); + + function balanceOf(address account) external view returns (uint256); + + function chi() external view returns (uint192); + + function convertToAssets(uint256 shares) external view returns (uint256); + + function convertToShares(uint256 assets) external view returns (uint256); + + function decimals() external view returns (uint8); + + function deny(address usr) external; + + function deposit(uint256 assets, address receiver) external returns (uint256); + + function deposit( + uint256 assets, + address receiver, + uint16 referral + ) external returns (uint256); + + function drip() external returns (uint256); + + function file(bytes32 what, uint256 data) external; + + function getImplementation() external view returns (address); + + function initialize() external; + + function maxDeposit(address account) external view returns (uint256); + + function maxMint(address account) external view returns (uint256); + + function maxRedeem(address owner) external view returns (uint256); + + function maxWithdraw(address owner) external view returns (uint256); + + function mint( + uint256 shares, + address receiver, + uint16 referral + ) external returns (uint256); + + function mint(uint256 shares, address receiver) external returns (uint256); + + function name() external view returns (string memory); + + function nonces(address account) external view returns (uint256); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + bytes memory signature + ) external; + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function previewDeposit(uint256 assets) external view returns (uint256); + + function previewMint(uint256 shares) external view returns (uint256); + + function previewRedeem(uint256 shares) external view returns (uint256); + + function previewWithdraw(uint256 assets) external view returns (uint256); + + function proxiableUUID() external view returns (bytes32); + + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256); + + function rely(address usr) external; + + function rho() external view returns (uint64); + + function ssr() external view returns (uint256); + + function symbol() external view returns (string memory); + + function totalAssets() external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function transfer(address to, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + function upgradeToAndCall( + address newImplementation, + bytes memory data + ) external payable; + + function usds() external view returns (address); + + function usdsJoin() external view returns (address); + + function vat() external view returns (address); + + function version() external view returns (string memory); + + function vow() external view returns (address); + + function wards(address account) external view returns (uint256); + + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256); +} diff --git a/src/interfaces/IWstETH.sol b/src/dependencies/IWstETH.sol similarity index 100% rename from src/interfaces/IWstETH.sol rename to src/dependencies/IWstETH.sol diff --git a/src/interfaces/IBaseTokenWrapper.sol b/src/interfaces/IBaseTokenWrapper.sol index 12b7976..0d525ad 100644 --- a/src/interfaces/IBaseTokenWrapper.sol +++ b/src/interfaces/IBaseTokenWrapper.sol @@ -61,6 +61,25 @@ interface IBaseTokenWrapper { PermitSignature calldata signature ) external returns (uint256); + /** + * @notice Borrows token from the Pool and unwraps it, sending to the recipient + * @param amount The amount of token to borrow + * @param referralCode Code used to register the integrator originating the operation, for potential rewards + */ + function borrowToken(uint256 amount, uint16 referralCode) external; + + /** + * @notice Borrows token from the Pool, unwraps it, and sends it to the recipient using EIP-2612 permit + * @param amount The amount of token to borrow + * @param referralCode Code used to register the integrator originating the operation, for potential rewards + * @param signature The EIP-712 signature data used for permit + */ + function borrowTokenWithPermit( + uint256 amount, + uint16 referralCode, + PermitSignature calldata signature + ) external; + /** * @notice Provides way for the contract owner to rescue ERC-20 tokens * @param token The address of the token to withdraw from this contract diff --git a/test/BaseTokenWrapper.t.sol b/test/BaseTokenWrapper.t.sol index be6be7f..5708948 100644 --- a/test/BaseTokenWrapper.t.sol +++ b/test/BaseTokenWrapper.t.sol @@ -3,11 +3,14 @@ pragma solidity ^0.8.10; import {Test} from 'forge-std/Test.sol'; import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; -import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; import {IAToken} from 'aave-v3-core/contracts/interfaces/IAToken.sol'; +import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; +import {ICreditDelegationToken} from 'aave-v3-core/contracts/interfaces/ICreditDelegationToken.sol'; import {MintableERC20} from 'aave-v3-core/contracts/mocks/tokens/MintableERC20.sol'; -import {BaseTokenWrapper} from '../src/BaseTokenWrapper.sol'; -import {IBaseTokenWrapper} from '../src/interfaces/IBaseTokenWrapper.sol'; +import {SigUtils} from './utils/SigUtils.sol'; + +import {IBaseTokenWrapper} from 'src/interfaces/IBaseTokenWrapper.sol'; +import {BaseTokenWrapper} from 'src/BaseTokenWrapper.sol'; interface IERC2612 { function nonces(address owner) external view returns (uint256); @@ -41,7 +44,9 @@ abstract contract BaseTokenWrapperTest is Test { BaseTokenWrapper tokenWrapper; address aTokenOut; uint256 tokenInDecimals; + address collateralAsset; bool permitSupported; + bool borrowSupported; constructor() { (ALICE, ALICE_KEY) = makeAddrAndKey('alice'); @@ -53,6 +58,7 @@ abstract contract BaseTokenWrapperTest is Test { function testSupplyToken() public { IERC20 tokenIn = IERC20(tokenWrapper.TOKEN_IN()); + IERC20 tokenOut = IERC20(tokenWrapper.TOKEN_OUT()); assertEq( tokenIn.balanceOf(ALICE), 0, @@ -80,6 +86,7 @@ abstract contract BaseTokenWrapperTest is Test { vm.stopPrank(); assertEq(tokenIn.balanceOf(ALICE), 0, 'Unexpected ending tokenIn balance'); + assertEq( suppliedAmount, IAToken(aTokenOut).balanceOf(ALICE), @@ -90,10 +97,17 @@ abstract contract BaseTokenWrapperTest is Test { 1, 'Unexpected ending aToken balance' ); + + assertEq( + tokenOut.allowance(address(tokenWrapper), pool), + 0, + 'Unexpected TOKEN_OUT allowance' + ); } function testSupplyTokenToOther() public { IERC20 tokenIn = IERC20(tokenWrapper.TOKEN_IN()); + IERC20 tokenOut = IERC20(tokenWrapper.TOKEN_OUT()); assertEq( tokenIn.balanceOf(ALICE), 0, @@ -136,10 +150,17 @@ abstract contract BaseTokenWrapperTest is Test { 1, 'Unexpected ending aToken balance' ); + + assertEq( + tokenOut.allowance(address(tokenWrapper), pool), + 0, + 'Unexpected TOKEN_OUT allowance' + ); } function testSupplyTokenWithPermit() public { IERC20 tokenIn = IERC20(tokenWrapper.TOKEN_IN()); + IERC20 tokenOut = IERC20(tokenWrapper.TOKEN_OUT()); assertEq( tokenIn.balanceOf(ALICE), 0, @@ -204,6 +225,12 @@ abstract contract BaseTokenWrapperTest is Test { 1, 'Unexpected ending aToken balance' ); + + assertEq( + tokenOut.allowance(address(tokenWrapper), pool), + 0, + 'Unexpected TOKEN_OUT allowance' + ); } else { vm.startPrank(ALICE); vm.expectRevert(); @@ -219,6 +246,7 @@ abstract contract BaseTokenWrapperTest is Test { function testPermitGriefingSupplyTokenWithPermit() public { IERC20 tokenIn = IERC20(tokenWrapper.TOKEN_IN()); + IERC20 tokenOut = IERC20(tokenWrapper.TOKEN_OUT()); assertEq( tokenIn.balanceOf(ALICE), 0, @@ -294,6 +322,12 @@ abstract contract BaseTokenWrapperTest is Test { 1, 'Unexpected ending aToken balance' ); + + assertEq( + tokenOut.allowance(address(tokenWrapper), pool), + 0, + 'Unexpected TOKEN_OUT allowance' + ); } else { vm.startPrank(ALICE); vm.expectRevert(); @@ -372,9 +406,6 @@ abstract contract BaseTokenWrapperTest is Test { 0, 'Unexpected starting tokenIn balance' ); - uint256 estimateFinalBalance = tokenWrapper.getTokenInForTokenOut( - aTokenBalance - ); vm.startPrank(ALICE); IAToken(aTokenOut).approve(address(tokenWrapper), aTokenBalance); @@ -638,6 +669,7 @@ abstract contract BaseTokenWrapperTest is Test { function testRescueETH() public { uint256 ethAmount = 100 ether; + vm.deal(address(tokenWrapper), 0); assertEq( address(tokenWrapper).balance, 0, @@ -678,6 +710,7 @@ abstract contract BaseTokenWrapperTest is Test { function testFuzzSupplyToken(uint256 amount, address referee) public { amount = bound(amount, 1, MAX_DEAL_AMOUNT); + vm.assume(IAToken(aTokenOut).balanceOf(referee) == 0); IERC20 tokenIn = IERC20(tokenWrapper.TOKEN_IN()); uint256 amountScaled = amount * 10 ** tokenInDecimals; @@ -688,16 +721,13 @@ abstract contract BaseTokenWrapperTest is Test { vm.startPrank(ALICE); tokenIn.approve(address(tokenWrapper), amountScaled); - uint256 suppliedAmount = tokenWrapper.supplyToken( - amountScaled, - referee, - REFERRAL_CODE - ); + tokenWrapper.supplyToken(amountScaled, referee, REFERRAL_CODE); vm.stopPrank(); assertEq(tokenIn.balanceOf(ALICE), 0, 'Unexpected ending tokenIn balance'); - assertLe( - estimateFinalBalance - IAToken(aTokenOut).balanceOf(referee), + assertApproxEqAbs( + estimateFinalBalance, + IAToken(aTokenOut).balanceOf(referee), 1, 'Unexpected ending aToken balance' ); @@ -714,9 +744,6 @@ abstract contract BaseTokenWrapperTest is Test { 0, 'Unexpected starting tokenIn balance' ); - uint256 estimateFinalBalance = tokenWrapper.getTokenInForTokenOut( - aTokenBalance - ); vm.startPrank(ALICE); IAToken(aTokenOut).approve(address(tokenWrapper), aTokenBalance); uint256 withdrawnAmount = tokenWrapper.withdrawToken(aTokenBalance, ALICE); @@ -731,6 +758,334 @@ abstract contract BaseTokenWrapperTest is Test { assertGt(withdrawnAmount, 0, 'Unexpected withdraw return/balance mismatch'); } + function testBorrowToken() public { + uint256 collateralAmount = 1000e18; + uint256 borrowAmount = 100e18; + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + deal(collateralAsset, ALICE, collateralAmount); + + vm.startPrank(ALICE); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0); + + ICreditDelegationToken(debtToken).approveDelegation( + address(tokenWrapper), + borrowAmount + ); + + if (borrowSupported) { + tokenWrapper.borrowToken(borrowAmount, 0); + uint256 borrowedAmount = tokenWrapper.getTokenInForTokenOut(borrowAmount); + assertEq( + IERC20(tokenWrapper.TOKEN_IN()).balanceOf(address(ALICE)), + borrowedAmount + ); + } else { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } + vm.stopPrank(); + } + + function testBorrowTokenZeroAmount() public { + uint256 collateralAmount = 1000e18; + uint256 borrowAmount = 0; + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + deal(collateralAsset, ALICE, collateralAmount); + + vm.startPrank(ALICE); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0); + + ICreditDelegationToken(debtToken).approveDelegation( + address(tokenWrapper), + borrowAmount + ); + + if (borrowSupported) { + vm.expectRevert('INSUFFICIENT_AMOUNT_TO_BORROW'); + tokenWrapper.borrowToken(borrowAmount, 0); + } else { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } + } + + function testBorrowTokenWithPermit() public { + uint256 borrowAmount = 100e18; + uint256 collateralAmount = 1000e18; + + (address alice, uint256 userPrivateKey) = makeAddrAndKey('ALICE'); + deal(collateralAsset, alice, collateralAmount); + + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + vm.startPrank(alice); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, alice, 0); + + uint256 deadline = block.timestamp + 1 hours; + uint256 nonce = IAToken(debtToken).nonces(alice); + + (uint8 v, bytes32 r, bytes32 s) = _signCreditDelegation( + userPrivateKey, + address(tokenWrapper), + borrowAmount, + nonce, + deadline, + debtToken + ); + IBaseTokenWrapper.PermitSignature memory signature = IBaseTokenWrapper + .PermitSignature({deadline: deadline, v: v, r: r, s: s}); + + if (borrowSupported) { + tokenWrapper.borrowTokenWithPermit(borrowAmount, 1, signature); + + vm.stopPrank(); + + uint256 borrowedAmount = tokenWrapper.getTokenInForTokenOut(borrowAmount); + assertEq( + IERC20(tokenWrapper.TOKEN_IN()).balanceOf(address(alice)), + borrowedAmount + ); + } else { + vm.expectRevert(); + tokenWrapper.borrowTokenWithPermit(borrowAmount, 1, signature); + } + } + + function testBorrowTokenWithPermitZeroAmount() public { + uint256 borrowAmount = 0; + uint256 collateralAmount = 1000e18; + + (address alice, uint256 userPrivateKey) = makeAddrAndKey('ALICE'); + deal(collateralAsset, alice, collateralAmount); + + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + vm.startPrank(alice); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, alice, 0); + + uint256 deadline = block.timestamp + 1 hours; + uint256 nonce = IAToken(debtToken).nonces(alice); + + (uint8 v, bytes32 r, bytes32 s) = _signCreditDelegation( + userPrivateKey, + address(tokenWrapper), + borrowAmount, + nonce, + deadline, + debtToken + ); + IBaseTokenWrapper.PermitSignature memory signature = IBaseTokenWrapper + .PermitSignature({deadline: deadline, v: v, r: r, s: s}); + if (borrowSupported) { + vm.expectRevert('INSUFFICIENT_AMOUNT_TO_BORROW'); + tokenWrapper.borrowTokenWithPermit(borrowAmount, 1, signature); + } else { + vm.expectRevert(); + tokenWrapper.borrowTokenWithPermit(borrowAmount, 1, signature); + } + } + + function testBorrowTokenWithPermitDeadlineExpired() public { + uint256 borrowAmount = 100e18; + uint256 collateralAmount = 1000e18; + + (address alice, uint256 userPrivateKey) = makeAddrAndKey('ALICE'); + deal(collateralAsset, alice, collateralAmount); + + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + vm.startPrank(alice); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, alice, 0); + + uint256 deadline = block.timestamp - 1; + uint256 nonce = IAToken(debtToken).nonces(alice); + + (uint8 v, bytes32 r, bytes32 s) = _signCreditDelegation( + userPrivateKey, + address(tokenWrapper), + borrowAmount, + nonce, + deadline, + debtToken + ); + IBaseTokenWrapper.PermitSignature memory signature = IBaseTokenWrapper + .PermitSignature({deadline: deadline, v: v, r: r, s: s}); + + if (borrowSupported) { + vm.expectRevert(); + tokenWrapper.borrowTokenWithPermit(borrowAmount, 1, signature); + } else { + vm.expectRevert(); + tokenWrapper.borrowTokenWithPermit(borrowAmount, 1, signature); + } + } + + function testBorrowTokenMoreThanUnwrappableInVault() public { + address vault = tokenWrapper.TOKEN_OUT(); + uint256 vaultUnderlying = IERC20(tokenWrapper.TOKEN_IN()).balanceOf(vault); + uint256 borrowAmount = tokenWrapper.getTokenOutForTokenIn(vaultUnderlying) * + 2; + uint256 collateralAmount = 1000e18; + + deal(collateralAsset, ALICE, collateralAmount); + + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + vm.startPrank(ALICE); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0); + + ICreditDelegationToken(debtToken).approveDelegation( + address(tokenWrapper), + borrowAmount + ); + + if (borrowSupported) { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } else { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } + } + + function testBorrowTokenInsufficientCollateral() public { + uint256 borrowAmount = 100e18; + + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + vm.startPrank(ALICE); + + ICreditDelegationToken(debtToken).approveDelegation( + address(tokenWrapper), + borrowAmount + ); + + if (borrowSupported) { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } else { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } + } + + function testBorrowTokenInsufficientDelegation() public { + uint256 borrowAmount = 100e18; + uint256 collateralAmount = 1000e18; + + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + deal(collateralAsset, ALICE, collateralAmount); + + vm.startPrank(ALICE); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0); + + ICreditDelegationToken(debtToken).approveDelegation( + address(tokenWrapper), + borrowAmount - 1 + ); + + if (borrowSupported) { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } else { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } + } + + function testFuzzBorrowToken(uint256 borrowAmount) public { + borrowAmount = bound(borrowAmount, 1, MAX_DEAL_AMOUNT); + borrowAmount *= 10 ** tokenInDecimals; + uint256 collateralAmount = borrowAmount * 10; + + address debtToken = IPool(pool) + .getReserveData(tokenWrapper.TOKEN_OUT()) + .variableDebtTokenAddress; + + deal(collateralAsset, ALICE, collateralAmount); + + vm.startPrank(ALICE); + + IERC20(collateralAsset).approve(address(pool), collateralAmount); + IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0); + + ICreditDelegationToken(debtToken).approveDelegation( + address(tokenWrapper), + borrowAmount + ); + + if (borrowSupported) { + tokenWrapper.borrowToken(borrowAmount, 0); + uint256 borrowedAmount = tokenWrapper.getTokenInForTokenOut(borrowAmount); + assertEq( + IERC20(tokenWrapper.TOKEN_IN()).balanceOf(ALICE), + borrowedAmount + ); + } else { + vm.expectRevert(); + tokenWrapper.borrowToken(borrowAmount, 0); + } + vm.stopPrank(); + } + + function _signCreditDelegation( + uint256 privateKey, + address delegatee, + uint256 value, + uint256 nonce, + uint256 deadline, + address debtToken + ) internal view returns (uint8 v, bytes32 r, bytes32 s) { + SigUtils.CreditDelegation memory creditDelegation = SigUtils + .CreditDelegation({ + delegatee: delegatee, + value: value, + nonce: nonce, + deadline: deadline + }); + + bytes32 domainSeparator = IAToken(debtToken).DOMAIN_SEPARATOR(); + bytes32 digest = SigUtils.getCreditDelegationTypedDataHash( + creditDelegation, + domainSeparator + ); + + return vm.sign(privateKey, digest); + } + function _dealTokenIn(address user, uint256 amount) internal virtual { deal(tokenWrapper.TOKEN_IN(), user, amount); } diff --git a/test/Generic4626Wrapper.t.sol b/test/Generic4626Wrapper.t.sol new file mode 100644 index 0000000..9f97a2e --- /dev/null +++ b/test/Generic4626Wrapper.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {IDefaultInterestRateStrategyV2} from 'aave-v3-core/contracts/interfaces/IDefaultInterestRateStrategyV2.sol'; +import {IAaveOracle} from 'aave-v3-core/contracts/interfaces/IAaveOracle.sol'; +import {MockAggregator} from 'aave-v3-core/contracts/mocks/oracle/CLAggregators/MockAggregator.sol'; +import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; +import {IPoolConfigurator} from 'aave-v3-core/contracts/interfaces/IPoolConfigurator.sol'; +import {ConfiguratorInputTypes} from 'aave-v3-core/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol'; +import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {MockERC4626} from './mocks/MockERC4626.sol'; +import {MockERC20} from './mocks/MockERC20.sol'; +import {BaseTokenWrapperTest} from './BaseTokenWrapper.t.sol'; + +import {Generic4626Wrapper} from 'src/Generic4626Wrapper.sol'; + +contract Generic4626WrapperTest is BaseTokenWrapperTest { + address constant ADMIN = AaveV3Ethereum.ACL_ADMIN; + IPoolConfigurator constant POOL_CONFIGURATOR = + AaveV3Ethereum.POOL_CONFIGURATOR; + IAaveOracle constant AAVE_ORACLE = AaveV3Ethereum.ORACLE; + MockERC20 unwrappedToken; + MockERC4626 wrappedToken; + address unwrapped; + address wrapped; + + function setUp() public { + vm.createSelectFork(vm.envString('ETH_RPC_URL'), 20784588); + pool = address(AaveV3Ethereum.POOL); + unwrappedToken = new MockERC20('UNWRAPPED'); + wrappedToken = new MockERC4626(unwrappedToken); + unwrapped = address(unwrappedToken); + wrapped = address(wrappedToken); + + // Put some underlying asset into the ERC4626 vault + unwrappedToken.approve(wrapped, 1e50); + wrappedToken.deposit(1e50, address(this)); + + // Airdrop some extra underlying asset to the vault + deal(unwrapped, address(this), 10e18); + unwrappedToken.transfer(wrapped, 10e18); + + tokenWrapper = new Generic4626Wrapper(unwrapped, wrapped, pool, OWNER); + tokenInDecimals = 18; + permitSupported = true; + borrowSupported = true; + collateralAsset = AaveV3EthereumAssets.WETH_UNDERLYING; + + _listAsset(wrapped); + + // Supply some of the new asset to pool + uint256 collateralAmount = 1000e18; + deal(wrapped, address(this), collateralAmount); + IERC20(wrapped).approve(address(pool), collateralAmount); + IPool(pool).supply(wrapped, collateralAmount, address(this), 0); + + aTokenOut = IPool(pool).getReserveData(wrapped).aTokenAddress; + } + + function testConstructor() public override { + Generic4626Wrapper tempTokenWrapper = new Generic4626Wrapper( + unwrapped, + wrapped, + pool, + OWNER + ); + assertEq(tempTokenWrapper.TOKEN_IN(), unwrapped, 'Unexpected TOKEN_IN'); + assertEq(tempTokenWrapper.TOKEN_OUT(), wrapped, 'Unexpected TOKEN_OUT'); + assertEq(address(tempTokenWrapper.POOL()), pool, 'Unexpected POOL'); + assertEq(tempTokenWrapper.owner(), OWNER, 'Unexpected owner'); + } + + function _listAsset(address underlying) internal { + IDefaultInterestRateStrategyV2.InterestRateData + memory interestRateData = IDefaultInterestRateStrategyV2 + .InterestRateData({ + optimalUsageRatio: 8000, + baseVariableBorrowRate: 1000, + variableRateSlope1: 1000, + variableRateSlope2: 1000 + }); + + ConfiguratorInputTypes.InitReserveInput[] + memory reserveInputs = new ConfiguratorInputTypes.InitReserveInput[](1); + reserveInputs[0] = ConfiguratorInputTypes.InitReserveInput({ + aTokenImpl: AaveV3Ethereum.DEFAULT_A_TOKEN_IMPL_REV_1, + stableDebtTokenImpl: AaveV3Ethereum.DEFAULT_STABLE_DEBT_TOKEN_IMPL_REV_1, + variableDebtTokenImpl: AaveV3Ethereum + .DEFAULT_VARIABLE_DEBT_TOKEN_IMPL_REV_1, + useVirtualBalance: true, + interestRateStrategyAddress: AaveV3EthereumAssets + .WETH_INTEREST_RATE_STRATEGY, + underlyingAsset: underlying, + treasury: address(AaveV3Ethereum.COLLECTOR), + incentivesController: AaveV3Ethereum.DEFAULT_INCENTIVES_CONTROLLER, + aTokenName: 'AaveWrapped', + aTokenSymbol: 'AWrapped', + variableDebtTokenName: 'VariableDebtWrapped', + variableDebtTokenSymbol: 'VWrapped', + stableDebtTokenName: 'StableDebtWrapped', + stableDebtTokenSymbol: 'SWrapped', + params: bytes(''), + interestRateData: abi.encode(interestRateData) + }); + + vm.startPrank(ADMIN); + POOL_CONFIGURATOR.initReserves(reserveInputs); + POOL_CONFIGURATOR.setReserveActive(underlying, true); + POOL_CONFIGURATOR.setReserveBorrowing(underlying, true); + + // Set asset oracle + MockAggregator oracle = new MockAggregator(1); + address[] memory assets = new address[](1); + assets[0] = underlying; + address[] memory sources = new address[](1); + sources[0] = address(oracle); + AAVE_ORACLE.setAssetSources(assets, sources); + vm.stopPrank(); + } +} diff --git a/test/SavingsDaiTokenWrapper.t.sol b/test/SavingsDaiTokenWrapper.t.sol index 6b49de7..3ecbc1a 100644 --- a/test/SavingsDaiTokenWrapper.t.sol +++ b/test/SavingsDaiTokenWrapper.t.sol @@ -1,22 +1,26 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.10; -import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; import {BaseTokenWrapperTest} from './BaseTokenWrapper.t.sol'; -import {SavingsDaiTokenWrapper} from '../src/SavingsDaiTokenWrapper.sol'; + +import {SavingsDaiTokenWrapper} from 'src/SavingsDaiTokenWrapper.sol'; contract SavingsDaiTokenWrapperTest is BaseTokenWrapperTest { - address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - address constant SDAI = 0x83F20F44975D03b1b09e64809B757c47f942BEeA; - address constant ASDAI = 0x4C612E3B15b96Ff9A6faED838F8d07d479a8dD4c; + address constant DAI = AaveV3EthereumAssets.DAI_UNDERLYING; + address constant SDAI = AaveV3EthereumAssets.sDAI_UNDERLYING; + address constant ASDAI = AaveV3EthereumAssets.sDAI_A_TOKEN; + address constant WETH = AaveV3EthereumAssets.WETH_UNDERLYING; function setUp() public { - vm.createSelectFork(vm.envString('ETH_RPC_URL')); - pool = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + vm.createSelectFork(vm.envString('ETH_RPC_URL'), 20784588); + pool = address(AaveV3Ethereum.POOL); tokenWrapper = new SavingsDaiTokenWrapper(DAI, SDAI, pool, OWNER); aTokenOut = ASDAI; tokenInDecimals = 18; permitSupported = false; + collateralAsset = AaveV3EthereumAssets.WETH_UNDERLYING; + borrowSupported = false; } function testConstructor() public override { @@ -30,15 +34,5 @@ contract SavingsDaiTokenWrapperTest is BaseTokenWrapperTest { assertEq(tempTokenWrapper.TOKEN_OUT(), SDAI, 'Unexpected TOKEN_OUT'); assertEq(address(tempTokenWrapper.POOL()), pool, 'Unexpected POOL'); assertEq(tempTokenWrapper.owner(), OWNER, 'Unexpected owner'); - assertEq( - IERC20(SDAI).allowance(address(tempTokenWrapper), pool), - type(uint256).max, - 'Unexpected TOKEN_OUT allowance' - ); - assertEq( - IERC20(DAI).allowance(address(tempTokenWrapper), SDAI), - type(uint256).max, - 'Unexpected TOKEN_IN allowance' - ); } } diff --git a/test/StakedEthTokenWrapper.t.sol b/test/StakedEthTokenWrapper.t.sol index ebbc19a..19474b0 100644 --- a/test/StakedEthTokenWrapper.t.sol +++ b/test/StakedEthTokenWrapper.t.sol @@ -1,22 +1,29 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.10; +import {AaveV2EthereumAssets} from 'aave-address-book/AaveV2Ethereum.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; + import {BaseTokenWrapperTest} from './BaseTokenWrapper.t.sol'; -import {StakedEthTokenWrapper} from '../src/StakedEthTokenWrapper.sol'; + +import {StakedEthTokenWrapper} from 'src/StakedEthTokenWrapper.sol'; contract StakedEthTokenWrapperTest is BaseTokenWrapperTest { - address constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; - address constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - address constant AWSTETH = 0x0B925eD163218f6662a35e0f0371Ac234f9E9371; + address constant STETH = AaveV2EthereumAssets.stETH_UNDERLYING; + address constant WSTETH = AaveV3EthereumAssets.wstETH_UNDERLYING; + address constant AWSTETH = AaveV3EthereumAssets.wstETH_A_TOKEN; + address constant WETH = AaveV3EthereumAssets.WETH_UNDERLYING; function setUp() public { - vm.createSelectFork(vm.envString('ETH_RPC_URL')); - pool = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + vm.createSelectFork(vm.envString('ETH_RPC_URL'), 20784588); + pool = address(AaveV3Ethereum.POOL); tokenWrapper = new StakedEthTokenWrapper(STETH, WSTETH, pool, OWNER); aTokenOut = AWSTETH; tokenInDecimals = 18; permitSupported = true; + collateralAsset = AaveV3EthereumAssets.WETH_UNDERLYING; + borrowSupported = false; } function testConstructor() public override { @@ -30,22 +37,13 @@ contract StakedEthTokenWrapperTest is BaseTokenWrapperTest { assertEq(tempTokenWrapper.TOKEN_OUT(), WSTETH, 'Unexpected TOKEN_OUT'); assertEq(address(tempTokenWrapper.POOL()), pool, 'Unexpected POOL'); assertEq(tempTokenWrapper.owner(), OWNER, 'Unexpected owner'); - assertEq( - IERC20(WSTETH).allowance(address(tempTokenWrapper), pool), - type(uint256).max, - 'Unexpected TOKEN_OUT allowance' - ); - assertEq( - IERC20(STETH).allowance(address(tempTokenWrapper), WSTETH), - type(uint256).max, - 'Unexpected TOKEN_IN allowance' - ); } function _dealTokenIn(address user, uint256 amount) internal override { - vm.deal(user, amount); - vm.prank(user); - (bool success, ) = STETH.call{value: amount}(''); - require(success); + // Custom deal function for stETH + deal(address(this), amount); + (bool success, ) = payable(tokenWrapper.TOKEN_IN()).call{value: amount}(''); + require(success, 'DEAL_FAILURE'); + IERC20(tokenWrapper.TOKEN_IN()).transfer(user, amount); } } diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol new file mode 100644 index 0000000..1a8b5bd --- /dev/null +++ b/test/mocks/MockERC20.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ERC20Permit} from 'openzeppelin/token/ERC20/extensions/ERC20Permit.sol'; +import {ERC20} from 'openzeppelin/token/ERC20/ERC20.sol'; + +contract MockERC20 is ERC20Permit { + constructor(string memory name) ERC20Permit(name) ERC20(name, name) { + _mint(msg.sender, 1e50); + } +} diff --git a/test/mocks/MockERC4626.sol b/test/mocks/MockERC4626.sol new file mode 100644 index 0000000..f505a8f --- /dev/null +++ b/test/mocks/MockERC4626.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from 'openzeppelin/token/ERC20/IERC20.sol'; +import {ERC20} from 'openzeppelin/token/ERC20/ERC20.sol'; +import {ERC4626} from 'openzeppelin/token/ERC20/extensions/ERC4626.sol'; + +contract MockERC4626 is ERC4626 { + constructor(IERC20 _asset) ERC20('SMOCK', 'SMOCK') ERC4626(_asset) {} +} diff --git a/test/utils/SigUtils.sol b/test/utils/SigUtils.sol new file mode 100644 index 0000000..43a3b93 --- /dev/null +++ b/test/utils/SigUtils.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +library SigUtils { + bytes32 public constant PERMIT_TYPEHASH = + keccak256( + 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)' + ); + + bytes32 private constant CREDIT_DELEGATION_TYPEHASH = + keccak256( + 'DelegationWithSig(address delegatee,uint256 value,uint256 nonce,uint256 deadline)' + ); + + struct Permit { + address owner; + address spender; + uint256 value; + uint256 nonce; + uint256 deadline; + } + + struct CreditDelegation { + address delegatee; + uint256 value; + uint256 nonce; + uint256 deadline; + } + + // computes the hash of a permit + function getStructHash( + Permit memory _permit + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + PERMIT_TYPEHASH, + _permit.owner, + _permit.spender, + _permit.value, + _permit.nonce, + _permit.deadline + ) + ); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function getPermitTypedDataHash( + Permit memory _permit, + bytes32 domainSeparator + ) internal pure returns (bytes32) { + return + keccak256( + abi.encodePacked( + '\x19\x01', + domainSeparator, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + _permit.owner, + _permit.spender, + _permit.value, + _permit.nonce, + _permit.deadline + ) + ) + ) + ); + } + + function getCreditDelegationTypedDataHash( + CreditDelegation memory _creditDelegation, + bytes32 domainSeparator + ) internal pure returns (bytes32) { + return + keccak256( + abi.encodePacked( + '\x19\x01', + domainSeparator, + keccak256( + abi.encode( + CREDIT_DELEGATION_TYPEHASH, + _creditDelegation.delegatee, + _creditDelegation.value, + _creditDelegation.nonce, + _creditDelegation.deadline + ) + ) + ) + ); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function getTypedDataHash( + Permit memory permit, + bytes32 domainSeperator + ) internal pure returns (bytes32) { + return + keccak256( + abi.encodePacked('\x19\x01', domainSeperator, getStructHash(permit)) + ); + } +}