Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vault proxy factory #367

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions script/Deployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {TrancheFactory} from "src/factories/TrancheFactory.sol";
import {ERC7540VaultFactory} from "src/factories/ERC7540VaultFactory.sol";
import {RestrictionManager} from "src/token/RestrictionManager.sol";
import {TransferProxyFactory} from "src/factories/TransferProxyFactory.sol";
import {VaultProxyFactory} from "src/factories/VaultProxyFactory.sol";
import {PoolManager} from "src/PoolManager.sol";
import {Escrow} from "src/Escrow.sol";
import {CentrifugeRouter} from "src/CentrifugeRouter.sol";
Expand All @@ -35,6 +36,7 @@ contract Deployer is Script {
address public restrictionManager;
address public trancheFactory;
address public transferProxyFactory;
address public vaultProxyFactory;

function deploy(address deployer) public {
// If no salt is provided, a pseudo-random salt is generated,
Expand All @@ -60,6 +62,7 @@ contract Deployer is Script {
gasService = new GasService(messageCost, proofCost, gasPrice, tokenPrice);
gateway = new Gateway(address(root), address(poolManager), address(investmentManager), address(gasService));
router = new CentrifugeRouter(address(routerEscrow), address(gateway), address(poolManager));
vaultProxyFactory = address(new VaultProxyFactory{salt: salt}(address(router)));
guardian = new Guardian(adminSafe, address(root), address(gateway));

_endorse();
Expand Down
83 changes: 83 additions & 0 deletions src/factories/VaultProxyFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.26;

import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol";
import {ICentrifugeRouter} from "src/interfaces/ICentrifugeRouter.sol";
import {IERC20} from "src/interfaces/IERC20.sol";
import {IERC7540Vault} from "src/interfaces/IERC7540.sol";
import {IVaultProxy, IVaultProxyFactory} from "src/interfaces/factories/IVaultProxy.sol";

contract VaultProxy is IVaultProxy {
IERC20 public immutable asset;
IERC20 public immutable share;
address public immutable user;
address public immutable vault;
ICentrifugeRouter public immutable router;

constructor(address router_, address vault_, address user_) {
asset = IERC20(IERC7540Vault(vault_).asset());
share = IERC20(IERC7540Vault(vault_).share());
user = user_;
vault = vault_;
router = ICentrifugeRouter(router_);
}

/// @inheritdoc IVaultProxy
function requestDeposit() external payable {
uint256 assets = asset.allowance(user, address(this));
require(assets > 0, "VaultProxy/zero-asset-allowance");
asset.transferFrom(user, address(router), assets);
router.requestDeposit{value: msg.value}(vault, assets, user, address(router), msg.value);
}

/// @inheritdoc IVaultProxy
function claimDeposit() external {
uint256 maxMint = IERC7540Vault(vault).maxMint(user);
IERC7540Vault(vault).mint(maxMint, address(user), user);
}

/// @inheritdoc IVaultProxy
function requestRedeem() external payable {
uint256 shares = share.allowance(user, user);
require(shares > 0, "VaultProxy/zero-share-allowance");
share.transferFrom(user, address(router), shares);
router.requestRedeem{value: msg.value}(vault, shares, user, address(router), msg.value);
}

/// @inheritdoc IVaultProxy
function claimRedeem() external {
uint256 maxWithdraw = IERC7540Vault(vault).maxWithdraw(address(this));
IERC7540Vault(vault).withdraw(maxWithdraw, address(user), address(this));
}
}

interface VaultProxyFactoryLike {
function newVaultProxy(address poolManager, bytes32 destination) external returns (address);
}

/// @title Vault investment proxy factory
/// @notice Used to deploy vault proxies that investors can give ERC20 approval for assets or shares
/// which anyone can then permissionlessly trigger the requests to the vaults to. Can be used
/// by integrations that can only support ERC20 approvals and not arbitrary contract calls.
contract VaultProxyFactory is IVaultProxyFactory {
address public immutable router;

/// @inheritdoc IVaultProxyFactory
mapping(bytes32 id => address proxy) public proxies;

constructor(address router_) {
router = router_;
}

/// @inheritdoc IVaultProxyFactory
function newVaultProxy(address vault, address user) public returns (address) {
bytes32 id = keccak256(abi.encodePacked(vault, user));
require(proxies[id] == address(0), "VaultProxyFactory/proxy-already-deployed");

address proxy = address(new VaultProxy(router, vault, user));
proxies[id] = proxy;

emit DeployVaultProxy(vault, user, proxy);
return proxy;
}
}
26 changes: 26 additions & 0 deletions src/interfaces/factories/IVaultProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.5.0;

interface IVaultProxy {
/// @dev Anyone can submit deposit request if there is USDC approval
function requestDeposit() external payable;

/// @dev Anyone can claim shares
function claimDeposit() external;

/// @dev Anyone can submit redeem request if there is share token approval
function requestRedeem() external payable;

/// @dev Anyone can claim assets
function claimRedeem() external;
}

interface IVaultProxyFactory {
event DeployVaultProxy(address indexed vault, address indexed user, address proxy);

/// @dev Lookup proxy by keccak256(vault,user)
function proxies(bytes32 id) external view returns (address proxy);

/// @dev Deploy new vault proxy
function newVaultProxy(address vault, address user) external returns (address);
}
32 changes: 32 additions & 0 deletions test/mocks/MockCentrifugeRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.26;

import "forge-std/Test.sol";
import "./Mock.sol";

contract MockCentrifugeRouter is Mock {
function requestDeposit(address vault, uint256 amount, address controller, address owner, uint256 topUpAmount)
external
payable
{
values_address["requestDeposit_vault"] = vault;
values_uint256["requestDeposit_amount"] = amount;
values_address["requestDeposit_controller"] = controller;
values_address["requestDeposit_owner"] = owner;
values_uint256["requestDeposit_topUpAmount"] = topUpAmount;
}

function requestRedeem(address vault, uint256 amount, address controller, address owner, uint256 topUpAmount)
external
payable
{
values_address["requestRedeem_vault"] = vault;
values_uint256["requestRedeem_amount"] = amount;
values_address["requestRedeem_controller"] = controller;
values_address["requestRedeem_owner"] = owner;
values_uint256["requestRedeem_topUpAmount"] = topUpAmount;
}

// Added to be ignored in coverage report
function test() public {}
}
18 changes: 18 additions & 0 deletions test/mocks/MockVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.26;

import "forge-std/Test.sol";
import "./Mock.sol";

contract MockVault is Mock {
address public immutable asset;
address public immutable share;

constructor(address asset_, address share_) {
asset = asset_;
share = share_;
}

// Added to be ignored in coverage report
function test() public {}
}
98 changes: 98 additions & 0 deletions test/unit/factories/VaultProxyFactory.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.26;

import {VaultProxy, VaultProxyFactory} from "src/factories/VaultProxyFactory.sol";
import {ERC20} from "src/token/ERC20.sol";
import {IERC7540Vault} from "src/interfaces/IERC7540.sol";
import "test/BaseTest.sol";

contract VaultProxyFactoryTest is BaseTest {
IERC7540Vault vault;
ERC20 asset = new ERC20(18);
ERC20 share = new ERC20(18);

function testVaultProxyCreation(address user) public {
vault = IERC7540Vault(deployVault(1, 18, restrictionManager, "", "", "1", 1, address(asset)));

VaultProxy proxy = VaultProxy(VaultProxyFactory(vaultProxyFactory).newVaultProxy(address(vault), user));
assertEq(VaultProxyFactory(vaultProxyFactory).router(), address(router));
assertEq(
VaultProxyFactory(vaultProxyFactory).proxies(keccak256(abi.encodePacked(address(vault), user))),
address(proxy)
);
assertEq(address(proxy.router()), address(router));
assertEq(proxy.vault(), address(vault));
assertEq(proxy.user(), user);

// Proxies cannot be deployed twice
vm.expectRevert(bytes("VaultProxyFactory/proxy-already-deployed"));
VaultProxyFactory(vaultProxyFactory).newVaultProxy(address(vault), user);
}

function testVaultProxyDeposit(uint256 amount) public {
amount = bound(amount, 1, type(uint128).max);

vault = IERC7540Vault(deployVault(1, 18, restrictionManager, "", "", "1", 1, address(asset)));
address user = makeAddr("user");

VaultProxy proxy = VaultProxy(VaultProxyFactory(vaultProxyFactory).newVaultProxy(address(vault), user));
centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), user, type(uint64).max);

asset.mint(user, amount);
vm.deal(address(this), 1 ether);

vm.expectRevert(bytes("VaultProxy/zero-asset-allowance"));
proxy.requestDeposit();

assertEq(asset.balanceOf(user), amount);
assertEq(asset.balanceOf(address(escrow)), 0);

vm.prank(user);
asset.approve(address(proxy), amount);

proxy.requestDeposit{value: 1 ether}();

assertEq(asset.balanceOf(user), 0);
assertEq(asset.balanceOf(address(escrow)), amount);

centrifugeChain.isFulfilledDepositRequest(
vault.poolId(), vault.trancheId(), bytes32(bytes20(user)), 1, uint128(amount), uint128(amount)
);

proxy.claimDeposit();
assertEq(share.balanceOf(user), amount);
}

function testVaultProxyRedeem(uint256 amount) public {
amount = bound(amount, 1, type(uint128).max);

vault = IERC7540Vault(deployVault(1, 18, restrictionManager, "", "", "1", 1, address(asset)));
address user = makeAddr("user");

VaultProxy proxy = VaultProxy(VaultProxyFactory(vaultProxyFactory).newVaultProxy(address(vault), user));
centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), address(proxy), type(uint64).max);

share.mint(user, amount);
vm.deal(address(this), 1 ether);

vm.expectRevert(bytes("VaultProxy/zero-share-allowance"));
proxy.requestRedeem();

assertEq(share.balanceOf(user), amount);
assertEq(share.balanceOf(address(router)), 0);

vm.prank(user);
share.approve(address(proxy), amount);

proxy.requestRedeem{value: 1 ether}();

assertEq(share.balanceOf(user), 0);
assertEq(share.balanceOf(address(router)), amount);

// assertEq(router.values_address("requestRedeem_vault"), address(vault));
// assertEq(router.values_uint256("requestRedeem_amount"), amount);
// assertEq(router.values_address("requestRedeem_controller"), user);
// assertEq(router.values_address("requestRedeem_owner"), address(router));
// assertEq(router.values_uint256("requestRedeem_topUpAmount"), 1 ether);
}
}
Loading