Skip to content

Commit

Permalink
Integrate setEndorsedOperator (#354)
Browse files Browse the repository at this point in the history
* Integrate setEndorsedOperator

* Add tests

* Remove opened storage

* Comment

* Cleanup
  • Loading branch information
hieronx committed Jul 9, 2024
1 parent e2baf69 commit a96cc01
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 53 deletions.
28 changes: 13 additions & 15 deletions src/CentrifugeRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ contract CentrifugeRouter is Auth, ICentrifugeRouter {
address constant UNSET_INITIATOR = address(1);
address internal _initiator = UNSET_INITIATOR;

/// @inheritdoc ICentrifugeRouter
mapping(address controller => mapping(address vault => bool)) public opened;

/// @inheritdoc ICentrifugeRouter
mapping(address controller => mapping(address vault => uint256 amount)) public lockedRequests;

Expand Down Expand Up @@ -53,6 +50,15 @@ contract CentrifugeRouter is Auth, ICentrifugeRouter {
SafeTransferLib.safeTransfer(token, to, amount);
}

// --- Enable interactions with the vault ---
function open(address vault) public protected {
IERC7540Vault(vault).setEndorsedOperator(_initiator, true);
}

function close(address vault) external protected {
IERC7540Vault(vault).setEndorsedOperator(_initiator, false);
}

// --- Deposit ---
/// @inheritdoc ICentrifugeRouter
function requestDeposit(address vault, uint256 amount, address controller, address owner, uint256 topUpAmount)
Expand Down Expand Up @@ -133,7 +139,8 @@ contract CentrifugeRouter is Auth, ICentrifugeRouter {
/// @inheritdoc ICentrifugeRouter
function claimDeposit(address vault, address receiver, address controller) external payable protected {
require(
controller == _initiator || (controller == receiver && opened[controller][vault] == true),
controller == _initiator
|| (controller == receiver && IERC7540Vault(vault).isOperator(controller, address(this))),
"CentrifugeRouter/invalid-sender"
);
uint256 maxDeposit = IERC7540Vault(vault).maxDeposit(controller);
Expand All @@ -153,8 +160,8 @@ contract CentrifugeRouter is Auth, ICentrifugeRouter {

/// @inheritdoc ICentrifugeRouter
function claimRedeem(address vault, address receiver, address controller) external payable protected {
bool permissionlesslyClaiming =
controller != _initiator && controller == receiver && opened[controller][vault] == true;
bool permissionlesslyClaiming = controller != _initiator && controller == receiver
&& IERC7540Vault(vault).isOperator(controller, address(this));

require(controller == _initiator || permissionlesslyClaiming, "CentrifugeRouter/invalid-sender");
uint256 maxRedeem = IERC7540Vault(vault).maxRedeem(controller);
Expand All @@ -169,15 +176,6 @@ contract CentrifugeRouter is Auth, ICentrifugeRouter {
}
}

// --- Manage permissionless claiming ---
function open(address vault) public protected {
opened[_initiator][vault] = true;
}

function close(address vault) external protected {
opened[_initiator][vault] = false;
}

// --- ERC20 permits ---
/// @inheritdoc ICentrifugeRouter
function permit(address asset, address spender, uint256 assets, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
Expand Down
42 changes: 28 additions & 14 deletions src/ERC7540Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
pragma solidity 0.8.26;

import {Auth} from "src/Auth.sol";
import {IRoot} from "src/interfaces/IRoot.sol";
import {EIP712Lib} from "src/libraries/EIP712Lib.sol";
import {ITranche} from "src/interfaces/token/ITranche.sol";
import {SignatureLib} from "src/libraries/SignatureLib.sol";
import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol";
import {IInvestmentManager} from "src/interfaces/IInvestmentManager.sol";
import {ITranche} from "src/interfaces/token/ITranche.sol";
import "src/interfaces/IERC7540.sol";
import "src/interfaces/IERC7575.sol";
import "src/interfaces/IERC20.sol";
Expand Down Expand Up @@ -35,13 +36,16 @@ contract ERC7540Vault is Auth, IERC7540Vault {
address public immutable share;
uint8 public immutable shareDecimals;

/// @notice Escrow contract for tokens
/// @dev For looking up endorsed contracts
IRoot public immutable root;

/// @dev Escrow contract for tokens
address public immutable escrow;

/// @notice Vault implementation contract
/// @dev Vault implementation contract
IInvestmentManager public manager;

/// @dev Requests for Centrifuge pool are non-transferable and all have ID = 0
/// @dev Requests for Centrifuge pool are non-transferable and all have ID = 0
uint256 constant REQUEST_ID = 0;

bytes32 private immutable nameHash;
Expand All @@ -59,12 +63,21 @@ contract ERC7540Vault is Auth, IERC7540Vault {
// --- Events ---
event File(bytes32 indexed what, address data);

constructor(uint64 poolId_, bytes16 trancheId_, address asset_, address share_, address escrow_, address manager_) {
constructor(
uint64 poolId_,
bytes16 trancheId_,
address asset_,
address share_,
address root_,
address escrow_,
address manager_
) {
poolId = poolId_;
trancheId = trancheId_;
asset = asset_;
share = share_;
shareDecimals = IERC20Metadata(share).decimals();
root = IRoot(root_);
escrow = escrow_;
manager = IInvestmentManager(manager_);

Expand All @@ -91,10 +104,7 @@ contract ERC7540Vault is Auth, IERC7540Vault {
// --- ERC-7540 methods ---
/// @inheritdoc IERC7540Deposit
function requestDeposit(uint256 assets, address controller, address owner) public returns (uint256) {
require(
owner == msg.sender || isOperator[owner][msg.sender] || manager.isGlobalOperator(address(this), msg.sender),
"ERC7540Vault/invalid-owner"
);
require(owner == msg.sender || isOperator[owner][msg.sender], "ERC7540Vault/invalid-owner");
require(IERC20(asset).balanceOf(owner) >= assets, "ERC7540Vault/insufficient-balance");

require(
Expand Down Expand Up @@ -212,6 +222,14 @@ contract ERC7540Vault is Auth, IERC7540Vault {
return true;
}

/// @inheritdoc IERC7540Vault
function setEndorsedOperator(address owner, bool approved) public virtual returns (bool) {
require(root.endorsed(msg.sender), "ERC7540Vault/sender-not-endorsed");
isOperator[owner][msg.sender] = approved;
emit OperatorSet(owner, msg.sender, approved);
return true;
}

/// @inheritdoc IAuthorizeOperator
function DOMAIN_SEPARATOR() public view returns (bytes32) {
return block.chainid == deploymentChainId
Expand Down Expand Up @@ -398,10 +416,6 @@ contract ERC7540Vault is Auth, IERC7540Vault {
}

function validateController(address controller) internal view {
require(
controller == msg.sender || isOperator[controller][msg.sender]
|| manager.isGlobalOperator(address(this), msg.sender),
"ERC7540Vault/invalid-controller"
);
require(controller == msg.sender || isOperator[controller][msg.sender], "ERC7540Vault/invalid-controller");
}
}
5 changes: 0 additions & 5 deletions src/InvestmentManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,6 @@ contract InvestmentManager is Auth, IInvestmentManager {
(, lastUpdated) = poolManager.getTranchePrice(vault_.poolId(), vault_.trancheId(), vault_.asset());
}

/// @inheritdoc IInvestmentManager
function isGlobalOperator(address, /* vault */ address user) public view returns (bool) {
return IRoot(root).endorsed(user);
}

// --- Vault claim functions ---
/// @inheritdoc IInvestmentManager
function deposit(address vault, uint256 assets, address receiver, address controller)
Expand Down
2 changes: 1 addition & 1 deletion src/factories/ERC7540VaultFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ contract ERC7540VaultFactory is Auth {
address investmentManager,
address[] calldata wards_
) public auth returns (address) {
ERC7540Vault vault = new ERC7540Vault(poolId, trancheId, asset, tranche, escrow, investmentManager);
ERC7540Vault vault = new ERC7540Vault(poolId, trancheId, asset, tranche, root, escrow, investmentManager);

vault.rely(root);
for (uint256 i = 0; i < wards_.length; i++) {
Expand Down
3 changes: 0 additions & 3 deletions src/interfaces/ICentrifugeRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ interface ICentrifugeRouter {
/// @notice TODO
function lockedRequests(address controller, address vault) external view returns (uint256 amount);

/// @notice Determines whether requests for a given controller and vault can be claimed by anyone (permissionlessly)
function opened(address controller, address vault) external view returns (bool);

// --- Administration ---
/// @notice TODO
function recoverTokens(address token, address to, uint256 amount) external;
Expand Down
3 changes: 3 additions & 0 deletions src/interfaces/IERC7540.sol
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ interface IERC7540Vault is
/// @notice Identifier of the tranche of the Centrifuge pool
function trancheId() external view returns (bytes16);

/// @notice TODO
function setEndorsedOperator(address owner, bool approved) external returns (bool);

/// @notice TODO
function onDepositClaimable(address owner, uint256 assets, uint256 shares) external;

Expand Down
3 changes: 0 additions & 3 deletions src/interfaces/IInvestmentManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,6 @@ interface IInvestmentManager is IMessageHandler {
/// @notice TODO
function priceLastUpdated(address vault) external view returns (uint64 lastUpdated);

/// @notice TODO
function isGlobalOperator(address, /* vault */ address user) external view returns (bool);

// --- Vault claim functions ---
/// @notice Processes owner's asset deposit / investment after the epoch has been executed on Centrifuge.
/// The asset required to fulfill the invest order is already locked in escrow upon calling
Expand Down
46 changes: 46 additions & 0 deletions test/integration/CentrifugeRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ contract CentrifugeRouterTest is BaseTest {
vm.expectRevert(bytes("Gateway/cannot-topup-with-nothing"));
router.requestDeposit(vault_, amount, self, self, 0);

vm.expectRevert(bytes("ERC7540Vault/invalid-owner"));
router.requestDeposit{value: 1 wei}(vault_, amount, self, self, 1 wei);

router.open(vault_);
vm.expectRevert(bytes("InvestmentManager/transfer-not-allowed"));
router.requestDeposit{value: 1 wei}(vault_, amount, self, self, 1 wei);
centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), self, type(uint64).max);
Expand Down Expand Up @@ -66,6 +70,30 @@ contract CentrifugeRouterTest is BaseTest {
assertApproxEqAbs(erc20.balanceOf(address(escrow)), amount, 1);
}

function testOpenCloseVaults() public {
address vault_ = deploySimpleVault();
ERC7540Vault vault = ERC7540Vault(vault_);
vm.label(vault_, "vault");

root.veto(address(router));
vm.expectRevert(bytes("ERC7540Vault/sender-not-endorsed"));
router.open(vault_);
assertEq(vault.isOperator(address(this), address(router)), false);

root.endorse(address(router));
router.open(vault_);
assertEq(vault.isOperator(address(this), address(router)), true);

root.veto(address(router));
vm.expectRevert(bytes("ERC7540Vault/sender-not-endorsed"));
router.close(vault_);
assertEq(vault.isOperator(address(this), address(router)), true);

root.endorse(address(router));
router.close(vault_);
assertEq(vault.isOperator(address(this), address(router)), false);
}

function testRouterAsyncDeposit(uint256 amount) public {
amount = uint128(bound(amount, 4, MAX_UINT128));
vm.assume(amount % 2 == 0);
Expand Down Expand Up @@ -113,6 +141,8 @@ contract CentrifugeRouterTest is BaseTest {
erc20.mint(self, amount);
erc20.approve(vault_, amount);
centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), self, type(uint64).max);
router.open(vault_);

uint256 fuel = estimateGas();
router.requestDeposit{value: fuel}(vault_, amount, self, self, fuel);

Expand Down Expand Up @@ -142,6 +172,10 @@ contract CentrifugeRouterTest is BaseTest {

uint256 fuel = estimateGas();
(ERC20 erc20X, ERC20 erc20Y, ERC7540Vault vault1, ERC7540Vault vault2) = setUpMultipleVaults(amount1, amount2);

router.open(address(vault1));
router.open(address(vault2));

router.requestDeposit{value: fuel}(address(vault1), amount1, self, self, fuel);
router.requestDeposit{value: fuel}(address(vault2), amount2, self, self, fuel);

Expand Down Expand Up @@ -179,6 +213,10 @@ contract CentrifugeRouterTest is BaseTest {
uint256 fuel = estimateGas();
// deposit
(ERC20 erc20X, ERC20 erc20Y, ERC7540Vault vault1, ERC7540Vault vault2) = setUpMultipleVaults(amount1, amount2);

router.open(address(vault1));
router.open(address(vault2));

router.requestDeposit{value: fuel}(address(vault1), amount1, self, self, fuel);
router.requestDeposit{value: fuel}(address(vault2), amount2, self, self, fuel);

Expand Down Expand Up @@ -225,6 +263,7 @@ contract CentrifugeRouterTest is BaseTest {
erc20.mint(self, amount);
erc20.approve(address(router), amount);
centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), self, type(uint64).max);
router.open(address(vault_));
router.lockDepositRequest(vault_, amount, self, self);

// multicall
Expand Down Expand Up @@ -252,6 +291,8 @@ contract CentrifugeRouterTest is BaseTest {
erc20.mint(self, amount);
erc20.approve(vault_, amount);
centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), self, type(uint64).max);
router.open(vault_);

uint256 fuel = estimateGas();
router.requestDeposit{value: fuel}(vault_, amount, self, self, fuel);

Expand Down Expand Up @@ -281,6 +322,9 @@ contract CentrifugeRouterTest is BaseTest {

(ERC20 erc20X, ERC20 erc20Y, ERC7540Vault vault1, ERC7540Vault vault2) = setUpMultipleVaults(amount1, amount2);

router.open(address(vault1));
router.open(address(vault2));

uint256 gas = estimateGas();
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(router.requestDeposit.selector, vault1, amount1, self, self, gas);
Expand Down Expand Up @@ -351,6 +395,7 @@ contract CentrifugeRouterTest is BaseTest {

address investor = makeAddr("investor");
centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), investor, type(uint64).max);
router.open(vault_);

erc20.mint(investor, amount);
vm.startPrank(investor);
Expand Down Expand Up @@ -426,6 +471,7 @@ contract CentrifugeRouterTest is BaseTest {

centrifugeChain.updateMember(vault.poolId(), vault.trancheId(), self, type(uint64).max);
erc20.approve(vault_, amount);
router.open(vault_);

uint256 gasLimit = estimateGas();
uint256 lessGas = gasLimit - 1;
Expand Down
3 changes: 2 additions & 1 deletion test/integration/Deposit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ contract DepositTest is BaseTest {
assertApproxEqAbs(erc20.balanceOf(address(escrow)), amount, 1);
}

function testDepositWithEndorsement(uint256 amount) public {
function testDepositAsEndorsedOperator(uint256 amount) public {
// If lower than 4 or odd, rounding down can lead to not receiving any tokens
amount = uint128(bound(amount, 4, MAX_UINT128));
vm.assume(amount % 2 == 0);
Expand Down Expand Up @@ -403,6 +403,7 @@ contract DepositTest is BaseTest {
// endorse router
root.endorse(router);
vm.startPrank(router); // try to claim deposit on behalf of user and set the wrong user as receiver
vault.setEndorsedOperator(address(this), true);
vault.deposit(amount, receiver, address(this));
vm.stopPrank();

Expand Down
11 changes: 0 additions & 11 deletions test/unit/CentrifugeRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,6 @@ contract CentrifugeRouterTest is BaseTest {
assertEq(erc20.balanceOf(self), amount);
}

function testOpenAndClose() public {
address vault_ = deploySimpleVault();
vm.label(vault_, "vault");

assertFalse(router.opened(self, vault_));
router.open(vault_);
assertTrue(router.opened(self, vault_));
router.close(vault_);
assertFalse(router.opened(self, vault_));
}

function testWrap() public {
uint256 amount = 150 * 10 ** 18;
uint256 balance = 100 * 10 ** 18;
Expand Down

0 comments on commit a96cc01

Please sign in to comment.