Skip to content

Commit

Permalink
Refactor price logic (#192)
Browse files Browse the repository at this point in the history
* Refactor price logic

* Remove redundant comments
  • Loading branch information
hieronx authored Oct 24, 2023
1 parent 0c01bf4 commit 296bdf6
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 149 deletions.
45 changes: 22 additions & 23 deletions src/InvestmentManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@ interface TrancheTokenLike is ERC20Like {
}

interface LiquidityPoolLike is ERC20Like {
function poolId() external returns (uint64);
function trancheId() external returns (bytes16);
function poolId() external view returns (uint64);
function trancheId() external view returns (bytes16);
function asset() external view returns (address);
function share() external view returns (address);
function updatePrice(uint256 price) external;
function latestPrice() external view returns (uint128);
}

interface AuthTransferLike {
Expand All @@ -48,6 +46,10 @@ interface PoolManagerLike {
function currencyIdToAddress(uint128 currencyId) external view returns (address);
function currencyAddressToId(address addr) external view returns (uint128);
function getTrancheToken(uint64 poolId, bytes16 trancheId) external view returns (address);
function getTrancheTokenPrice(uint64 poolId, bytes16 trancheId, address currencyAddress)
external
view
returns (uint256 price, uint64 computedAt);
function getLiquidityPool(uint64 poolId, bytes16 trancheId, uint128 currencyId) external view returns (address);
function isAllowedAsInvestmentCurrency(uint64 poolId, address currencyAddress) external view returns (bool);
}
Expand Down Expand Up @@ -279,18 +281,6 @@ contract InvestmentManager is Auth {
}

// --- Incoming message handling ---
/// @notice Update the price of a tranche token
/// @dev This also happens automatically on incoming order executions,
/// but this incoming call from Centrifuge can be used to update the price
/// whenever the price is outdated but no orders are outstanding.
function updateTrancheTokenPrice(uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price)
public
onlyGateway
{
address liquidityPool = poolManager.getLiquidityPool(poolId, trancheId, currencyId);
LiquidityPoolLike(liquidityPool).updatePrice(uint256(price));
}

function handleExecutedCollectInvest(
uint64 poolId,
bytes16 trancheId,
Expand All @@ -309,8 +299,6 @@ contract InvestmentManager is Auth {
state.maxMint = state.maxMint + trancheTokenPayout;
state.remainingDepositRequest = remainingInvestOrder;

LiquidityPoolLike(liquidityPool).updatePrice(_calculatePrice(liquidityPool, currencyPayout, trancheTokenPayout));

// Mint to escrow. Recipient can claim by calling withdraw / redeem
ERC20Like trancheToken = ERC20Like(LiquidityPoolLike(liquidityPool).share());
trancheToken.mint(address(escrow), trancheTokenPayout);
Expand Down Expand Up @@ -341,8 +329,6 @@ contract InvestmentManager is Auth {
state.maxWithdraw = state.maxWithdraw + currencyPayout;
state.remainingRedeemRequest = remainingRedeemOrder;

LiquidityPoolLike(liquidityPool).updatePrice(_calculatePrice(liquidityPool, currencyPayout, trancheTokenPayout));

// Transfer currency to user escrow to claim on withdraw/redeem,
// and burn redeemed tranche tokens from escrow
userEscrow.transferIn(poolManager.currencyIdToAddress(currencyId), address(escrow), user, currencyPayout);
Expand Down Expand Up @@ -458,12 +444,18 @@ contract InvestmentManager is Auth {

// --- View functions ---
function convertToShares(address liquidityPool, uint256 _assets) public view returns (uint256 shares) {
uint256 latestPrice = LiquidityPoolLike(liquidityPool).latestPrice();
LiquidityPoolLike liquidityPool_ = LiquidityPoolLike(liquidityPool);
(uint256 latestPrice,) = poolManager.getTrancheTokenPrice(
liquidityPool_.poolId(), liquidityPool_.trancheId(), liquidityPool_.asset()
);
shares = uint256(_calculateTrancheTokenAmount(_assets.toUint128(), liquidityPool, latestPrice));
}

function convertToAssets(address liquidityPool, uint256 _shares) public view returns (uint256 assets) {
uint256 latestPrice = LiquidityPoolLike(liquidityPool).latestPrice();
LiquidityPoolLike liquidityPool_ = LiquidityPoolLike(liquidityPool);
(uint256 latestPrice,) = poolManager.getTrancheTokenPrice(
liquidityPool_.poolId(), liquidityPool_.trancheId(), liquidityPool_.asset()
);
assets = uint256(_calculateCurrencyAmount(_shares.toUint128(), liquidityPool, latestPrice));
}

Expand Down Expand Up @@ -503,6 +495,13 @@ contract InvestmentManager is Auth {
trancheTokenAmount = uint256(investments[liquidityPool][user].remainingRedeemRequest);
}

function exchangeRateLastUpdated(address liquidityPool) public view returns (uint64 lastUpdated) {
LiquidityPoolLike liquidityPool_ = LiquidityPoolLike(liquidityPool);
(, lastUpdated) = poolManager.getTrancheTokenPrice(
liquidityPool_.poolId(), liquidityPool_.trancheId(), liquidityPool_.asset()
);
}

// --- Liquidity Pool processing functions ---
/// @notice Processes owner's currency deposit / investment after the epoch has been executed on Centrifuge.
/// The currency required to fulfill the invest order is already locked in escrow upon calling
Expand Down Expand Up @@ -643,7 +642,7 @@ contract InvestmentManager is Auth {

function _calculatePrice(uint256 currencyAmountInPriceDecimals, uint256 trancheTokenAmountInPriceDecimals)
internal
view
pure
returns (uint256 price)
{
if (currencyAmountInPriceDecimals == 0 || trancheTokenAmountInPriceDecimals == 0) {
Expand Down
19 changes: 5 additions & 14 deletions src/LiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface ManagerLike {
function cancelRedeemRequest(address lp, address operator) external;
function pendingDepositRequest(address lp, address operator) external view returns (uint256);
function pendingRedeemRequest(address lp, address operator) external view returns (uint256);
function exchangeRateLastUpdated(address liquidityPool) external view returns (uint64 lastUpdated);
}

/// @title Liquidity Pool
Expand Down Expand Up @@ -63,19 +64,12 @@ contract LiquidityPool is Auth, IERC7540 {
/// @notice Liquidity Pool business logic implementation contract
ManagerLike public manager;

/// @notice Tranche token price, denominated in the asset
uint256 public latestPrice;

/// @notice Timestamp of the last price update
uint256 public lastPriceUpdate;

// --- Events ---
event File(bytes32 indexed what, address data);
event DecreaseDepositRequest(address indexed sender, uint256 assets);
event DecreaseRedeemRequest(address indexed sender, uint256 shares);
event CancelDepositRequest(address indexed sender);
event CancelRedeemRequest(address indexed sender);
event PriceUpdate(uint256 price);

constructor(uint64 poolId_, bytes16 trancheId_, address asset_, address share_, address escrow_, address manager_) {
poolId = poolId_;
Expand Down Expand Up @@ -270,6 +264,10 @@ contract LiquidityPool is Auth, IERC7540 {
emit CancelRedeemRequest(msg.sender);
}

function exchangeRateLastUpdated() public view returns (uint64) {
return manager.exchangeRateLastUpdated(address(this));
}

// --- ERC-20 overrides ---
function name() public view returns (string memory) {
return share.name();
Expand Down Expand Up @@ -317,13 +315,6 @@ contract LiquidityPool is Auth, IERC7540 {
return abi.decode(data, (bool));
}

// --- Pricing ---
function updatePrice(uint256 price) public auth {
latestPrice = price;
lastPriceUpdate = block.timestamp;
emit PriceUpdate(price);
}

// --- Helpers ---

/// @dev In case of unsuccessful tx, parse the revert message
Expand Down
40 changes: 40 additions & 0 deletions src/PoolManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ struct Tranche {
/// @dev Each tranche can have multiple liquidity pools deployed,
/// each linked to a unique investment currency (asset)
mapping(address currencyAddress => address liquidityPool) liquidityPools;
/// @dev Each tranche has a price per liquidity pool
mapping(address liquidityPool => TrancheTokenPrice) prices;
}

struct TrancheTokenPrice {
uint256 price;
uint64 computedAt;
}

/// @dev Temporary storage that is only present between addTranche and deployTranche
Expand Down Expand Up @@ -100,6 +107,13 @@ contract PoolManager is Auth {
event DeployTranche(uint64 indexed poolId, bytes16 indexed trancheId, address indexed token);
event AddCurrency(uint128 indexed currency, address indexed currencyAddress);
event DeployLiquidityPool(uint64 indexed poolId, bytes16 indexed trancheId, address indexed liquidityPool);
event PriceUpdate(
uint64 indexed poolId,
bytes16 indexed trancheId,
uint128 indexed currency,
uint256 price,
uint64 priceComputedAt
);
event TransferCurrency(address indexed currencyAddress, bytes32 indexed recipient, uint128 amount);
event TransferTrancheTokensToCentrifuge(
uint64 indexed poolId, bytes16 indexed trancheId, bytes32 destinationAddress, uint128 amount
Expand Down Expand Up @@ -261,6 +275,22 @@ contract PoolManager is Auth {
trancheToken.file("symbol", tokenSymbol);
}

function updateTrancheTokenPrice(
uint64 poolId,
bytes16 trancheId,
uint128 currencyId,
uint128 price,
uint64 computedAt
) public onlyGateway {
Tranche storage tranche = pools[poolId].tranches[trancheId];
require(tranche.token != address(0), "PoolManager/tranche-does-not-exist");

address currencyAddress = currencyIdToAddress[currencyId];
tranche.prices[currencyAddress] = TrancheTokenPrice(price, computedAt);

emit PriceUpdate(poolId, trancheId, currencyId, price, computedAt);
}

function updateMember(uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) public onlyGateway {
require(user != address(escrow), "PoolManager/escrow-member-cannot-be-updated");

Expand Down Expand Up @@ -419,6 +449,16 @@ contract PoolManager is Auth {
return pools[poolId].tranches[trancheId].liquidityPools[currencyAddress];
}

function getTrancheTokenPrice(uint64 poolId, bytes16 trancheId, address currencyAddress)
public
view
returns (uint256 price, uint64 computedAt)
{
TrancheTokenPrice memory value = pools[poolId].tranches[trancheId].prices[currencyAddress];
price = value.price;
computedAt = value.computedAt;
}

function isAllowedAsInvestmentCurrency(uint64 poolId, address currencyAddress) public view returns (bool) {
uint128 currency = currencyAddressToId[currencyAddress];
if (currency == 0) {
Expand Down
12 changes: 9 additions & 3 deletions src/gateway/Gateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {Messages} from "./Messages.sol";
import {Auth} from "./../util/Auth.sol";

interface InvestmentManagerLike {
function updateTrancheTokenPrice(uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price) external;
function handleExecutedDecreaseInvestOrder(
uint64 poolId,
bytes16 trancheId,
Expand Down Expand Up @@ -69,6 +68,13 @@ interface PoolManagerLike {
string memory tokenName,
string memory tokenSymbol
) external;
function updateTrancheTokenPrice(
uint64 poolId,
bytes16 trancheId,
uint128 currencyId,
uint128 price,
uint64 computedAt
) external;
function addCurrency(uint128 currency, address currencyAddress) external;
function handleTransfer(uint128 currency, address recipient, uint128 amount) external;
function handleTransferTrancheTokens(uint64 poolId, bytes16 trancheId, address destinationAddress, uint128 amount)
Expand Down Expand Up @@ -310,9 +316,9 @@ contract Gateway is Auth {
(uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) = Messages.parseUpdateMember(message);
poolManager.updateMember(poolId, trancheId, user, validUntil);
} else if (Messages.isUpdateTrancheTokenPrice(message)) {
(uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price) =
(uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price, uint64 computedAt) =
Messages.parseUpdateTrancheTokenPrice(message);
investmentManager.updateTrancheTokenPrice(poolId, trancheId, currencyId, price);
poolManager.updateTrancheTokenPrice(poolId, trancheId, currencyId, price, computedAt);
} else if (Messages.isTransfer(message)) {
(uint128 currency, address recipient, uint128 amount) = Messages.parseIncomingTransfer(message);
poolManager.handleTransfer(currency, recipient, amount);
Expand Down
18 changes: 11 additions & 7 deletions src/gateway/Messages.sol
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,16 @@ library Messages {
* 9-24: trancheId (16 bytes)
* 25-40: currency (uint128 = 16 bytes)
* 41-56: price (uint128 = 16 bytes)
* 57-64: computedAt (uint64 = 8 bytes)
*/
function formatUpdateTrancheTokenPrice(uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price)
internal
pure
returns (bytes memory)
{
return abi.encodePacked(uint8(Call.UpdateTrancheTokenPrice), poolId, trancheId, currencyId, price);
function formatUpdateTrancheTokenPrice(
uint64 poolId,
bytes16 trancheId,
uint128 currencyId,
uint128 price,
uint64 computedAt
) internal pure returns (bytes memory) {
return abi.encodePacked(uint8(Call.UpdateTrancheTokenPrice), poolId, trancheId, currencyId, price, computedAt);
}

function isUpdateTrancheTokenPrice(bytes memory _msg) internal pure returns (bool) {
Expand All @@ -241,12 +244,13 @@ library Messages {
function parseUpdateTrancheTokenPrice(bytes memory _msg)
internal
pure
returns (uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price)
returns (uint64 poolId, bytes16 trancheId, uint128 currencyId, uint128 price, uint64 computedAt)
{
poolId = BytesLib.toUint64(_msg, 1);
trancheId = BytesLib.toBytes16(_msg, 9);
currencyId = BytesLib.toUint128(_msg, 25);
price = BytesLib.toUint128(_msg, 41);
computedAt = BytesLib.toUint64(_msg, 57);
}

/*
Expand Down
63 changes: 1 addition & 62 deletions test/InvestmentManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "./TestSetup.t.sol";

interface LiquidityPoolLike {
function latestPrice() external view returns (uint128);
function lastPriceUpdate() external view returns (uint256);
function priceComputedAt() external view returns (uint64);
}

contract InvestmentManagerTest is TestSetup {
Expand Down Expand Up @@ -48,65 +48,4 @@ contract InvestmentManagerTest is TestSetup {
vm.expectRevert(bytes("Auth/not-authorized"));
investmentManager.file("poolManager", random);
}

function testUpdatingTokenPriceWorks(
uint64 poolId,
uint8 decimals,
uint128 currencyId,
string memory tokenName,
string memory tokenSymbol,
bytes16 trancheId,
uint128 price
) public {
decimals = uint8(bound(decimals, 1, 18));
vm.assume(poolId > 0);
vm.assume(trancheId > 0);
vm.assume(currencyId > 0);
centrifugeChain.addPool(poolId); // add pool
centrifugeChain.addTranche(poolId, trancheId, tokenName, tokenSymbol, decimals); // add tranche
centrifugeChain.addCurrency(currencyId, address(erc20)); // add currency
centrifugeChain.allowInvestmentCurrency(poolId, currencyId);

poolManager.deployTranche(poolId, trancheId);
LiquidityPoolLike lPool = LiquidityPoolLike(poolManager.deployLiquidityPool(poolId, trancheId, address(erc20)));

centrifugeChain.updateTrancheTokenPrice(poolId, trancheId, currencyId, price);
assertEq(lPool.latestPrice(), price);
assertEq(lPool.lastPriceUpdate(), block.timestamp);
}

function testUpdatingTokenPriceAsNonRouterFails(
uint64 poolId,
uint8 decimals,
uint128 currency,
string memory tokenName,
string memory tokenSymbol,
bytes16 trancheId,
uint128 price
) public {
decimals = uint8(bound(decimals, 1, 18));
vm.assume(currency > 0);
ERC20 erc20 = _newErc20("X's Dollar", "USDX", 18);
centrifugeChain.addPool(poolId); // add pool
centrifugeChain.addTranche(poolId, trancheId, tokenName, tokenSymbol, decimals); // add tranche
centrifugeChain.addCurrency(currency, address(erc20));
centrifugeChain.allowInvestmentCurrency(poolId, currency);
poolManager.deployTranche(poolId, trancheId);
poolManager.deployLiquidityPool(poolId, trancheId, address(erc20));

vm.expectRevert(bytes("InvestmentManager/not-the-gateway"));
investmentManager.updateTrancheTokenPrice(poolId, trancheId, currency, price);
}

function testUpdatingTokenPriceForNonExistentTrancheFails(
uint64 poolId,
bytes16 trancheId,
uint128 currencyId,
uint128 price
) public {
centrifugeChain.addPool(poolId);

vm.expectRevert(bytes(""));
centrifugeChain.updateTrancheTokenPrice(poolId, trancheId, currencyId, price);
}
}
Loading

0 comments on commit 296bdf6

Please sign in to comment.