Skip to content

Commit ddd63e2

Browse files
authored
Merge pull request #66 from GenerationSoftware/main
Vault V2
2 parents 10eb9ff + 97f5fd1 commit ddd63e2

16 files changed

+267
-215
lines changed

package-lock.json

Lines changed: 99 additions & 112 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@generationsoftware/pt-v5-vault",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"description": "PoolTogether V5 Vault contracts",
55
"author": {
66
"name": "G9 Software Inc.",
@@ -24,8 +24,8 @@
2424
},
2525
"devDependencies": {
2626
"husky": "8.0.3",
27-
"lint-staged": "14.0.1",
28-
"prettier": "3.0.3",
27+
"lint-staged": "15.0.0",
28+
"prettier": "2.8.8",
2929
"prettier-plugin-solidity": "1.1.3",
3030
"solhint": "3.6.2",
3131
"solhint-plugin-prettier": "0.0.5"

src/Vault.sol

Lines changed: 33 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IClaimable } from "pt-v5-claimable-interface/interfaces/IClaimable.sol"
1616
import { VaultHooks } from "./interfaces/IVaultHooks.sol";
1717

1818
/**
19-
* @title PoolTogether V5 Vault
19+
* @title PoolTogether V5 Vault (Version 2)
2020
* @author PoolTogether Inc. & G9 Software Inc.
2121
* @notice Vault extends the ERC4626 standard and is the entry point for users interacting with a V5 pool.
2222
* Users deposit an underlying asset (i.e. USDC) in this contract and receive in exchange an ERC20 token
@@ -25,7 +25,7 @@ import { VaultHooks } from "./interfaces/IVaultHooks.sol";
2525
* This yield is sold for prize tokens (i.e. POOL) via the Liquidator and captured by the PrizePool to be awarded to depositors.
2626
* @dev Balances are stored in the TwabController contract.
2727
*/
28-
contract Vault is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable {
28+
contract VaultV2 is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable {
2929
using Math for uint256;
3030
using SafeCast for uint256;
3131
using SafeERC20 for IERC20;
@@ -301,6 +301,23 @@ contract Vault is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable
301301
/// @notice Emitted when a prize is claimed for the zero address.
302302
error ClaimRecipientZeroAddress();
303303

304+
/**
305+
* @notice Emitted when the caller of a permit function is not the owner of the assets being permitted.
306+
* @param caller The address of the caller
307+
* @param owner The address of the owner
308+
*/
309+
error PermitCallerNotOwner(address caller, address owner);
310+
311+
/**
312+
* @notice Emitted when a permit call on the underlying asset failed to set the spending allowance.
313+
* @dev This is likely thrown when the underlying asset does not support permit, but has a fallback function.
314+
* @param owner The owner of the assets
315+
* @param spender The spender of the assets
316+
* @param amount The amount of assets permitted
317+
* @param allowance The allowance after the permit was called
318+
*/
319+
error PermitAllowanceNotSet(address owner, address spender, uint256 amount, uint256 allowance);
320+
304321
/* ============ Modifiers ============ */
305322

306323
/// @notice Modifier reverting if the Vault is under-collateralized.
@@ -528,7 +545,17 @@ contract Vault is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable
528545
bytes32 _r,
529546
bytes32 _s
530547
) external returns (uint256) {
531-
_permit(IERC20Permit(address(_asset)), _owner, address(this), _assets, _deadline, _v, _r, _s);
548+
if (_owner != msg.sender) {
549+
revert PermitCallerNotOwner(msg.sender, _owner);
550+
}
551+
552+
IERC20Permit(address(_asset)).permit(_owner, address(this), _assets, _deadline, _v, _r, _s);
553+
554+
uint256 _allowance = _asset.allowance(_owner, address(this));
555+
if (_allowance != _assets) {
556+
revert PermitAllowanceNotSet(_owner, address(this), _assets, _allowance);
557+
}
558+
532559
return _depositAssets(_assets, _owner, _owner, false);
533560
}
534561

@@ -670,8 +697,7 @@ contract Vault is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable
670697

671698
_onlyVaultCollateralized(_depositedAssets, _withdrawableAssets);
672699

673-
uint256 _availableYield = _withdrawableAssets -
674-
_convertToAssets(_depositedAssets, _depositedAssets, _withdrawableAssets, Math.Rounding.Down);
700+
uint256 _availableYield = _withdrawableAssets - _depositedAssets;
675701

676702
if (_shares > _availableYield) revert YieldFeeGTAvailableYield(_shares, _availableYield);
677703
if (_shares > _yieldFeeShares) revert YieldFeeGTAvailableShares(_shares, _yieldFeeShares);
@@ -1073,38 +1099,6 @@ contract Vault is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable
10731099
_collateralAssets == 0 ? 0 : _shares.mulDiv(_collateralAssets, _depositedAssets, _rounding);
10741100
}
10751101

1076-
/**
1077-
* @notice Convert Vault shares to YieldVault shares.
1078-
* @param _shares Vault shares to convert
1079-
* @param _depositedAssets Assets deposited into the YieldVault
1080-
* @param _withdrawableAssets Assets withdrawable from the YieldVault
1081-
* @param _maxRedeemableYVShares Maximum amount of shares that can be redeemed from the YieldVault
1082-
* @param _rounding Rounding mode (i.e. down or up)
1083-
* @return uint256 YieldVault shares
1084-
*/
1085-
function _convertSharesToYVShares(
1086-
uint256 _shares,
1087-
uint256 _depositedAssets,
1088-
uint256 _withdrawableAssets,
1089-
uint256 _maxRedeemableYVShares,
1090-
Math.Rounding _rounding
1091-
) internal view returns (uint256) {
1092-
if (_shares == 0) {
1093-
return _shares;
1094-
}
1095-
1096-
if (_depositedAssets == 0) {
1097-
return _yieldVault.convertToShares(_shares);
1098-
}
1099-
1100-
uint256 _redeemableShares = _isVaultCollateralized(_depositedAssets, _withdrawableAssets)
1101-
? _depositedAssets // shares are backed 1:1 by assets, no need to convert to YieldVault shares
1102-
: _maxRedeemableYVShares;
1103-
1104-
return
1105-
_redeemableShares == 0 ? 0 : _shares.mulDiv(_redeemableShares, _depositedAssets, _rounding);
1106-
}
1107-
11081102
/* ============ Max / Preview Functions ============ */
11091103

11101104
/**
@@ -1317,11 +1311,9 @@ contract Vault is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable
13171311
uint256 _yieldVaultShares;
13181312

13191313
if (!_vaultCollateralized) {
1320-
_yieldVaultShares = _convertSharesToYVShares(
1321-
_shares,
1322-
_totalSupply(),
1323-
_totalAssets(),
1314+
_yieldVaultShares = _shares.mulDiv(
13241315
_yieldVault.maxRedeem(address(this)),
1316+
_totalSupply(),
13251317
Math.Rounding.Down
13261318
);
13271319
}
@@ -1349,32 +1341,6 @@ contract Vault is IERC4626, ERC20Permit, ILiquidationSource, IClaimable, Ownable
13491341
return _assets;
13501342
}
13511343

1352-
/* ============ Permit Functions ============ */
1353-
1354-
/**
1355-
* @notice Approve `_spender` to spend `_assets` of `_owner`'s `_permitAsset` via signature.
1356-
* @param _permitAsset Address of the asset to approve
1357-
* @param _owner Address of the owner of the asset
1358-
* @param _spender Address of the spender of the asset
1359-
* @param _assets Amount of assets to approve
1360-
* @param _deadline Timestamp after which the approval is no longer valid
1361-
* @param _v V part of the secp256k1 signature
1362-
* @param _r R part of the secp256k1 signature
1363-
* @param _s S part of the secp256k1 signature
1364-
*/
1365-
function _permit(
1366-
IERC20Permit _permitAsset,
1367-
address _owner,
1368-
address _spender,
1369-
uint256 _assets,
1370-
uint256 _deadline,
1371-
uint8 _v,
1372-
bytes32 _r,
1373-
bytes32 _s
1374-
) internal {
1375-
_permitAsset.permit(_owner, _spender, _assets, _deadline, _v, _r, _s);
1376-
}
1377-
13781344
/* ============ State Functions ============ */
13791345

13801346
/**

src/VaultFactory.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,22 @@ import { IERC20, IERC4626 } from "openzeppelin/token/ERC20/extensions/ERC4626.so
55

66
import { PrizePool } from "pt-v5-prize-pool/PrizePool.sol";
77

8-
import { Vault } from "./Vault.sol";
8+
import { VaultV2 as Vault } from "./Vault.sol";
99

1010
/**
11-
* @title PoolTogether V5 Vault Factory
11+
* @title PoolTogether V5 Vault Factory (Version 2)
1212
* @author PoolTogether Inc. & G9 Software Inc.
1313
* @notice Factory contract for deploying new vaults using a standard underlying ERC4626 yield vault.
1414
*/
15-
contract VaultFactory {
15+
contract VaultFactoryV2 {
1616
/* ============ Events ============ */
1717

1818
/**
1919
* @notice Emitted when a new Vault has been deployed by this factory.
2020
* @param vault Address of the vault that was deployed
2121
* @param vaultFactory Address of the VaultFactory that deployed `vault`
2222
*/
23-
event NewFactoryVault(Vault indexed vault, VaultFactory indexed vaultFactory);
23+
event NewFactoryVault(Vault indexed vault, VaultFactoryV2 indexed vaultFactory);
2424

2525
/* ============ Variables ============ */
2626

@@ -31,7 +31,7 @@ contract VaultFactory {
3131
* @notice Mapping to verify if a Vault has been deployed via this factory.
3232
* @dev Vault address => boolean
3333
*/
34-
mapping(Vault => bool) public deployedVaults;
34+
mapping(address => bool) public deployedVaults;
3535

3636
/**
3737
* @notice Mapping to store deployer nonces for CREATE2
@@ -80,9 +80,9 @@ contract VaultFactory {
8080
);
8181

8282
allVaults.push(_vault);
83-
deployedVaults[_vault] = true;
83+
deployedVaults[address(_vault)] = true;
8484

85-
emit NewFactoryVault(_vault, VaultFactory(address(this)));
85+
emit NewFactoryVault(_vault, VaultFactoryV2(address(this)));
8686

8787
return address(_vault);
8888
}

src/interfaces/IVaultHooks.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ interface IVaultHooks {
2121
* @param winner The user who won the prize and for whom this hook is attached
2222
* @param tier The tier of the prize
2323
* @param prizeIndex The index of the prize in the tier
24+
* @param fee The fee portion of the prize that will be allocated to the claimer
25+
* @param feeRecipient The recipient of the claim fee
2426
* @return address The address of the recipient of the prize
2527
*/
2628
function beforeClaimPrize(
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.19;
3+
4+
import { ERC20Mock } from "openzeppelin/mocks/ERC20Mock.sol";
5+
6+
contract ERC20PermitFallbackMock is ERC20Mock {
7+
constructor() ERC20Mock() {}
8+
9+
fallback() external payable {
10+
// catch-all for functions that don't exist, like permit
11+
}
12+
}

test/fuzz/Vault.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Strings } from "openzeppelin/utils/Strings.sol";
1010
import { PrizePool } from "pt-v5-prize-pool/PrizePool.sol";
1111
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
1212

13-
import { Vault } from "../../src/Vault.sol";
13+
import { VaultV2 as Vault } from "../../src/Vault.sol";
1414

1515
import { LiquidationPairMock } from "../contracts/mock/LiquidationPairMock.sol";
1616
import { LiquidationRouterMock } from "../contracts/mock/LiquidationRouterMock.sol";

test/unit/Vault/Deposit.t.sol

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BrokenToken } from "brokentoken/BrokenToken.sol";
55
import { IERC4626 } from "openzeppelin/token/ERC20/extensions/ERC4626.sol";
66

77
import { IERC20, UnitBaseSetup } from "../../utils/UnitBaseSetup.t.sol";
8-
import "../../../src/Vault.sol";
8+
import { VaultV2 as Vault } from "../../../src/Vault.sol";
99

1010
contract VaultDepositTest is UnitBaseSetup, BrokenToken {
1111
/* ============ Events ============ */
@@ -147,7 +147,7 @@ contract VaultDepositTest is UnitBaseSetup, BrokenToken {
147147
vm.stopPrank();
148148
}
149149

150-
function testDepositWithPermitByThirdParty() external {
150+
function testDepositWithPermitByThirdParty_CallerNotOwner() external {
151151
vm.startPrank(alice);
152152

153153
uint256 _amount = 1000e18;
@@ -163,22 +163,11 @@ contract VaultDepositTest is UnitBaseSetup, BrokenToken {
163163

164164
vm.stopPrank();
165165

166-
vm.expectEmit();
167-
emit Transfer(address(0), alice, _amount);
168-
169-
vm.expectEmit();
170-
emit Deposit(alice, alice, _amount, _amount);
166+
vm.expectRevert(
167+
abi.encodeWithSelector(Vault.PermitCallerNotOwner.selector, address(this), alice)
168+
);
171169

172170
_depositWithPermit(vault, _amount, alice, _v, _r, _s);
173-
174-
assertEq(vault.balanceOf(alice), _amount);
175-
176-
assertEq(twabController.balanceOf(address(vault), alice), _amount);
177-
assertEq(twabController.delegateBalanceOf(address(vault), alice), _amount);
178-
179-
assertEq(underlyingAsset.balanceOf(address(yieldVault)), _amount);
180-
assertEq(yieldVault.balanceOf(address(vault)), _amount);
181-
assertEq(yieldVault.totalSupply(), _amount);
182171
}
183172

184173
/* ============ Deposit - Errors ============ */

test/unit/Vault/DepositBrokenToken.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pragma solidity ^0.8.19;
33

44
import { UnitBrokenTokenBaseSetup } from "../../utils/UnitBrokenTokenBaseSetup.t.sol";
5-
import "../../../src/Vault.sol";
5+
import { VaultV2 as Vault } from "../../../src/Vault.sol";
66

77
contract VaultDepositBrokenTokenTest is UnitBrokenTokenBaseSetup {
88
/* ============ Events ============ */
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.19;
3+
4+
import { IERC4626 } from "openzeppelin/token/ERC20/extensions/ERC4626.sol";
5+
6+
import { IERC20, UnitBaseSetup } from "../../utils/UnitBaseSetup.t.sol";
7+
import { VaultV2 as Vault } from "../../../src/Vault.sol";
8+
9+
import { ERC20PermitFallbackMock } from "../../contracts/mock/ERC20PermitFallbackMock.sol";
10+
import { ERC20PermitMock } from "../../contracts/mock/ERC20PermitMock.sol";
11+
12+
contract VaultDepositTest is UnitBaseSetup {
13+
/* ============ Events ============ */
14+
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
15+
16+
event Sponsor(address indexed caller, uint256 assets, uint256 shares);
17+
18+
event Sweep(address indexed caller, uint256 assets);
19+
20+
event Transfer(address indexed from, address indexed to, uint256 value);
21+
22+
function setUpUnderlyingAsset() public override returns (ERC20PermitMock) {
23+
return ERC20PermitMock(address(new ERC20PermitFallbackMock()));
24+
}
25+
26+
/* ============ Tests ============ */
27+
28+
function testDepositWithoutPermit_AllowanceNotSet() external {
29+
vm.startPrank(alice);
30+
31+
uint256 _amount = 1000e18;
32+
underlyingAsset.mint(alice, _amount);
33+
34+
underlyingAsset.approve(address(vault), _amount * 2);
35+
36+
vm.expectRevert(
37+
abi.encodeWithSelector(
38+
Vault.PermitAllowanceNotSet.selector,
39+
alice,
40+
address(vault),
41+
_amount,
42+
_amount * 2
43+
)
44+
);
45+
46+
uint8 _v = 0;
47+
bytes32 _r = bytes32(0);
48+
bytes32 _s = bytes32(0);
49+
vault.depositWithPermit(_amount, alice, block.timestamp, _v, _r, _s);
50+
51+
vm.stopPrank();
52+
}
53+
54+
function testForceDepositWithoutPermitByThirdParty() external {
55+
vm.startPrank(alice);
56+
57+
uint256 _amount = 1000e18;
58+
underlyingAsset.mint(alice, _amount);
59+
60+
underlyingAsset.approve(address(vault), _amount);
61+
62+
vm.stopPrank();
63+
64+
vm.expectRevert(
65+
abi.encodeWithSelector(Vault.PermitCallerNotOwner.selector, address(this), alice)
66+
);
67+
68+
uint8 _v = 0;
69+
bytes32 _r = bytes32(0);
70+
bytes32 _s = bytes32(0);
71+
vault.depositWithPermit(_amount, alice, block.timestamp, _v, _r, _s);
72+
}
73+
74+
function testFailPermitSign() external {
75+
vm.startPrank(alice);
76+
77+
uint256 _amount = 1000e18;
78+
underlyingAsset.mint(alice, _amount);
79+
80+
(uint8 _v, bytes32 _r, bytes32 _s) = _signPermit(
81+
underlyingAsset,
82+
vault,
83+
_amount,
84+
alice,
85+
alicePrivateKey
86+
);
87+
88+
vm.stopPrank();
89+
}
90+
}

0 commit comments

Comments
 (0)