Skip to content

Commit

Permalink
Lock utilised capacity (#39)
Browse files Browse the repository at this point in the history
* refactor: give more general name to locked up funds mapping

* refactor: use more generic require messages for locked tokens

* feat: update locked funds with utilised capacity

* refactor: general locked balance getter; test defaults improvements

* test: not able to trade with locked funds

* feat: only entity admin can pay dividend

* feat: locked funds and utilised capacity are factored by collateral ratio

* 📝 docs: update natspec generated markdowns

* test: harden collateral ratio test case

* feat: add collateral ratio updated event

* test: fix cell update test

* feat: ensure enough balance available when increasing collateral ratio
  • Loading branch information
amarinkovic committed Dec 12, 2022
1 parent 2e34d37 commit f8b2d59
Show file tree
Hide file tree
Showing 17 changed files with 370 additions and 234 deletions.
20 changes: 20 additions & 0 deletions docs/facets/ITokenizedVaultFacet.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,23 @@ Transfer dividends to the entity
|`guid` | bytes32 | Globally unique identifier of a dividend distribution.
|`amount` | uint256 | the mamount of the dividend token to be distributed to NAYMS token holders.|
<br></br>
### getLockedBalance
Get the amount of tokens that an entity has for sale in the marketplace.
```solidity
function getLockedBalance(
bytes32 _entityId,
bytes32 _tokenId
) external returns (uint256 amount)
```
#### Arguments:
| Argument | Type | Description |
| --- | --- | --- |
|`_entityId` | bytes32 | Unique platform ID of the entity.
|`_tokenId` | bytes32 | The ID assigned to an external token.
|
<br></br>
#### Returns:
| Type | Description |
| --- | --- |
|`amount` | of tokens that the entity has for sale in the marketplace.|
<br></br>
20 changes: 0 additions & 20 deletions docs/facets/IUserFacet.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,3 @@ Gets the entity related to the user
| --- | --- |
|`entityId` | Unique platform ID of the entity|
<br></br>
### getBalanceOfTokensForSale
Get the amount of tokens that an entity has for sale in the marketplace.
```solidity
function getBalanceOfTokensForSale(
bytes32 _entityId,
bytes32 _tokenId
) external returns (uint256 amount)
```
#### Arguments:
| Argument | Type | Description |
| --- | --- | --- |
|`_entityId` | bytes32 | Unique platform ID of the entity.
|`_tokenId` | bytes32 | The ID assigned to an external token.
|
<br></br>
#### Returns:
| Type | Description |
| --- | --- |
|`amount` | of tokens that the entity has for sale in the marketplace.|
<br></br>
2 changes: 1 addition & 1 deletion src/diamonds/nayms/AppStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ struct AppStorage {
uint16 premiumCommissionSTMBP;
// A policy can pay out additional commissions on premiums to entities having a variety of roles on the policy

mapping(bytes32 => mapping(bytes32 => uint256)) marketLockedBalances; // to keep track of an owner's tokens that are on sale in the marketplace, ownerId => lockedTokenId => amount
mapping(bytes32 => mapping(bytes32 => uint256)) lockedBalances; // keep track of token balance that is locked, ownerId => tokenId => lockedAmount
}

library LibAppStorage {
Expand Down
16 changes: 15 additions & 1 deletion src/diamonds/nayms/facets/TokenizedVaultFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Modifiers } from "../Modifiers.sol";
import { LibConstants } from "../libs/LibConstants.sol";
import { LibHelpers } from "../libs/LibHelpers.sol";
import { LibTokenizedVault } from "../libs/LibTokenizedVault.sol";
import { LibACL } from "../libs/LibACL.sol";
import { LibObject } from "../libs/LibObject.sol";
import { LibEntity } from "../libs/LibEntity.sol";

Expand Down Expand Up @@ -108,9 +109,22 @@ contract TokenizedVaultFacet is Modifiers {
bytes32 entityId = LibObject._getParentFromAddress(msg.sender);
bytes32 dividendTokenId = LibEntity._getEntityInfo(entityId).assetId;

require(LibACL._isInGroup(LibHelpers._getIdForAddress(msg.sender), entityId, LibHelpers._stringToBytes32(LibConstants.GROUP_ENTITY_ADMINS)), "not the entity's admin");
require(
LibACL._isInGroup(LibHelpers._getIdForAddress(msg.sender), entityId, LibHelpers._stringToBytes32(LibConstants.GROUP_ENTITY_ADMINS)),
"payDividendFromEntity: not the entity's admin"
);
require(LibTokenizedVault._internalBalanceOf(entityId, dividendTokenId) >= amount, "payDividendFromEntity: insufficient balance");

LibTokenizedVault._payDividend(guid, entityId, entityId, dividendTokenId, amount);
}

/**
* @notice Get the amount of tokens that an entity has for sale in the marketplace.
* @param _entityId Unique platform ID of the entity.
* @param _tokenId The ID assigned to an external token.
* @return amount of tokens that the entity has for sale in the marketplace.
*/
function getLockedBalance(bytes32 _entityId, bytes32 _tokenId) external view returns (uint256 amount) {
amount = LibTokenizedVault._getLockedBalance(_entityId, _tokenId);
}
}
10 changes: 0 additions & 10 deletions src/diamonds/nayms/facets/UserFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,4 @@ contract UserFacet is Modifiers {
function getEntity(bytes32 _userId) external view returns (bytes32 entityId) {
entityId = LibObject._getParent(_userId);
}

/**
* @notice Get the amount of tokens that an entity has for sale in the marketplace.
* @param _entityId Unique platform ID of the entity.
* @param _tokenId The ID assigned to an external token.
* @return amount of tokens that the entity has for sale in the marketplace.
*/
function getBalanceOfTokensForSale(bytes32 _entityId, bytes32 _tokenId) external view returns (uint256 amount) {
amount = LibMarket._getBalanceOfTokensForSale(_entityId, _tokenId);
}
}
8 changes: 8 additions & 0 deletions src/diamonds/nayms/interfaces/ITokenizedVaultFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,12 @@ interface ITokenizedVaultFacet {
* @param amount the mamount of the dividend token to be distributed to NAYMS token holders.
*/
function payDividendFromEntity(bytes32 guid, uint256 amount) external;

/**
* @notice Get the amount of tokens that an entity has for sale in the marketplace.
* @param _entityId Unique platform ID of the entity.
* @param _tokenId The ID assigned to an external token.
* @return amount of tokens that the entity has for sale in the marketplace.
*/
function getLockedBalance(bytes32 _entityId, bytes32 _tokenId) external view returns (uint256 amount);
}
8 changes: 0 additions & 8 deletions src/diamonds/nayms/interfaces/IUserFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,4 @@ interface IUserFacet {
* @return entityId Unique platform ID of the entity
*/
function getEntity(bytes32 _userId) external view returns (bytes32 entityId);

/**
* @notice Get the amount of tokens that an entity has for sale in the marketplace.
* @param _entityId Unique platform ID of the entity.
* @param _tokenId The ID assigned to an external token.
* @return amount of tokens that the entity has for sale in the marketplace.
*/
function getBalanceOfTokensForSale(bytes32 _entityId, bytes32 _tokenId) external view returns (uint256 amount);
}
42 changes: 31 additions & 11 deletions src/diamonds/nayms/libs/LibEntity.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ library LibEntity {
event EntityUpdated(bytes32 entityId);
event SimplePolicyCreated(bytes32 indexed id, bytes32 entityId);
event TokenSaleStarted(bytes32 indexed entityId, uint256 offerId);
event CollateralRatioUpdated(bytes32 indexed entityId, uint256 collateralRatio, uint256 utilizedCapacity);

/**
* @dev If an entity passes their checks to create a policy, ensure that the entity's capacity is appropriately decreased by the amount of capital that will be tied to the new policy being created.
*/
function _validateSimplePolicyCreation(bytes32 _entityId, SimplePolicy calldata simplePolicy) internal view returns (uint256 updatedUtilizedCapacity) {
function _validateSimplePolicyCreation(bytes32 _entityId, SimplePolicy calldata simplePolicy) internal view {
// The policy's limit cannot be 0. If a policy's limit is zero, this essentially means the policy doesn't require any capital, which doesn't make business sense.
require(simplePolicy.limit > 0, "limit not > 0");
require(LibAdmin._isSupportedExternalToken(simplePolicy.asset), "external token is not supported");

bool isEntityAdmin = LibACL._isInGroup(LibHelpers._getSenderId(), _entityId, LibHelpers._stringToBytes32(LibConstants.GROUP_ENTITY_ADMINS));
require(isEntityAdmin, "must be entity admin");
Expand All @@ -58,18 +60,15 @@ library LibEntity {
// require(entity.collateralRatio > 0 && entity.maxCapacity > 0, "currency disabled");

// Calculate the entity's utilized capacity after it writes this policy.
updatedUtilizedCapacity = entity.utilizedCapacity + simplePolicy.limit;
uint256 updatedUtilizedCapacity = entity.utilizedCapacity + ((simplePolicy.limit * entity.collateralRatio) / LibConstants.BP_FACTOR);

// The entity must have enough capacity available to write this policy.
// An entity is not able to write an additional policy that will utilize its capacity beyond its assigned max capacity.
require(entity.maxCapacity >= updatedUtilizedCapacity, "not enough available capacity");

// Calculate the entity's required capital for its capacity utilization based on its collateral requirements.
uint256 capitalRequirementForUpdatedUtilizedCapacity = (updatedUtilizedCapacity * entity.collateralRatio) / LibConstants.BP_FACTOR;

// The entity's balance must be >= to the updated capacity requirement
// todo: business only wants to count the entity's balance that was raised from the participation token sale and not its total balance
require(LibTokenizedVault._internalBalanceOf(_entityId, simplePolicy.asset) >= capitalRequirementForUpdatedUtilizedCapacity, "not enough capital");
require(LibTokenizedVault._internalBalanceOf(_entityId, simplePolicy.asset) >= updatedUtilizedCapacity, "not enough capital");

require(simplePolicy.startDate >= block.timestamp, "start date < block.timestamp");
require(simplePolicy.maturationDate > simplePolicy.startDate, "start date > maturation date");
Expand Down Expand Up @@ -105,9 +104,13 @@ library LibEntity {
}
require(_stakeholders.entityIds.length == _stakeholders.signatures.length, "incorrect number of signatures");

// note: An entity's updated utilized capacity <= max capitalization check is done in _validateSimplePolicyCreation().
// Update state with the entity's updated utilized capacity.
s.entities[_entityId].utilizedCapacity = _validateSimplePolicyCreation(_entityId, _simplePolicy);
_validateSimplePolicyCreation(_entityId, _simplePolicy);

Entity storage entity = s.entities[_entityId];
uint256 factoredLimit = (_simplePolicy.limit * entity.collateralRatio) / LibConstants.BP_FACTOR;

entity.utilizedCapacity += factoredLimit;
s.lockedBalances[_entityId][entity.assetId] += factoredLimit;

LibObject._createObject(_policyId, _entityId, _dataHash);
s.simplePolicies[_policyId] = _simplePolicy;
Expand Down Expand Up @@ -210,16 +213,33 @@ library LibEntity {

function _updateEntity(bytes32 _entityId, Entity memory _entity) internal {
AppStorage storage s = LibAppStorage.diamondStorage();

// Cannot update a non-existing entity's metadata.
if (s.existingEntities[_entityId] == false) {
revert EntityDoesNotExist(_entityId);
}

validateEntity(_entity);

// assetId change not allowed
uint256 oldCollateralRatio = s.entities[_entityId].collateralRatio;
uint256 oldUtilizedCapacity = s.entities[_entityId].utilizedCapacity;
bytes32 originalAssetId = s.entities[_entityId].assetId;

s.entities[_entityId] = _entity;
s.entities[_entityId].assetId = originalAssetId;
s.entities[_entityId].assetId = originalAssetId; // assetId change not allowed

// if it's a cell, and collateral ratio changed
if (_entity.assetId != 0 && _entity.collateralRatio != oldCollateralRatio) {
uint256 newUtilizedCapacity = (oldUtilizedCapacity * _entity.collateralRatio) / oldCollateralRatio;
uint256 newLockedBalance = s.lockedBalances[_entityId][_entity.assetId] - oldUtilizedCapacity + newUtilizedCapacity;

require(LibTokenizedVault._internalBalanceOf(_entityId, _entity.assetId) >= newLockedBalance, "collateral ratio invalid, not enough balance");

s.entities[_entityId].utilizedCapacity = newUtilizedCapacity;
s.lockedBalances[_entityId][_entity.assetId] = newLockedBalance;

emit CollateralRatioUpdated(_entityId, _entity.collateralRatio, s.entities[_entityId].utilizedCapacity);
}

emit EntityUpdated(_entityId);
}
Expand Down
13 changes: 4 additions & 9 deletions src/diamonds/nayms/libs/LibMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ library LibMarket {
marketInfo.state = LibConstants.OFFER_STATE_ACTIVE;

// lock tokens!
s.marketLockedBalances[_creator][_sellToken] += _sellAmount;
s.lockedBalances[_creator][_sellToken] += _sellAmount;
}

s.offers[lastOfferId] = marketInfo;
Expand Down Expand Up @@ -282,7 +282,7 @@ library LibMarket {
}
}

s.marketLockedBalances[s.offers[_offerId].creator][s.offers[_offerId].sellToken] -= _buyAmount;
s.lockedBalances[s.offers[_offerId].creator][s.offers[_offerId].sellToken] -= _buyAmount;

LibTokenizedVault._internalTransfer(s.offers[_offerId].creator, _takerId, s.offers[_offerId].sellToken, _buyAmount);
LibTokenizedVault._internalTransfer(_takerId, s.offers[_offerId].creator, s.offers[_offerId].buyToken, _sellAmount);
Expand Down Expand Up @@ -335,7 +335,7 @@ library LibMarket {
// unlock the remaining sell amount back to creator
if (marketInfo.sellAmount > 0) {
// note nothing is transferred since tokens for sale are UN-escrowed. Just unlock!
s.marketLockedBalances[s.offers[_offerId].creator][s.offers[_offerId].sellToken] -= marketInfo.sellAmount;
s.lockedBalances[s.offers[_offerId].creator][s.offers[_offerId].sellToken] -= marketInfo.sellAmount;
}

// don't emit event stating market order is canceled if the market order was executed and fulfilled
Expand Down Expand Up @@ -378,7 +378,7 @@ library LibMarket {

// note: add restriction to not be able to sell tokens that are already for sale
// maker must own sell amount and it must not be locked
require(s.tokenBalances[_sellToken][_entityId] - s.marketLockedBalances[_entityId][_sellToken] >= _sellAmount, "tokens locked in market");
require(s.tokenBalances[_sellToken][_entityId] - s.lockedBalances[_entityId][_sellToken] >= _sellAmount, "tokens locked");

// must have a valid fee schedule
require(_feeSchedule == LibConstants.FEE_SCHEDULE_PLATFORM_ACTION || _feeSchedule == LibConstants.FEE_SCHEDULE_STANDARD, "fee schedule invalid");
Expand Down Expand Up @@ -438,9 +438,4 @@ library LibMarket {
AppStorage storage s = LibAppStorage.diamondStorage();
return s.offers[_offerId].state == LibConstants.OFFER_STATE_ACTIVE;
}

function _getBalanceOfTokensForSale(bytes32 _entityId, bytes32 _tokenId) internal view returns (uint256 amount) {
AppStorage storage s = LibAppStorage.diamondStorage();
return s.marketLockedBalances[_entityId][_tokenId];
}
}
9 changes: 7 additions & 2 deletions src/diamonds/nayms/libs/LibSimplePolicy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,15 @@ library LibSimplePolicy {

function releaseFunds(bytes32 _policyId) private {
AppStorage storage s = LibAppStorage.diamondStorage();
bytes32 entityId = LibObject._getParent(_policyId);

SimplePolicy storage simplePolicy = s.simplePolicies[_policyId];
Entity storage entity = s.entities[LibObject._getParent(_policyId)];
Entity storage entity = s.entities[entityId];

uint256 policyLockedAmount = (simplePolicy.limit * entity.collateralRatio) / LibConstants.BP_FACTOR;
entity.utilizedCapacity -= policyLockedAmount;
s.lockedBalances[entityId][entity.assetId] -= policyLockedAmount;

entity.utilizedCapacity -= simplePolicy.limit;
simplePolicy.fundsLocked = false;
}
}
13 changes: 9 additions & 4 deletions src/diamonds/nayms/libs/LibTokenizedVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ library LibTokenizedVault {
) internal returns (bool success) {
AppStorage storage s = LibAppStorage.diamondStorage();

if (s.marketLockedBalances[_from][_tokenId] > 0) {
require(s.tokenBalances[_tokenId][_from] - s.marketLockedBalances[_from][_tokenId] >= _amount, "_internalTransferFrom: tokens for sale in mkt");
if (s.lockedBalances[_from][_tokenId] > 0) {
require(s.tokenBalances[_tokenId][_from] - s.lockedBalances[_from][_tokenId] >= _amount, "_internalTransferFrom: tokens locked");
} else {
require(s.tokenBalances[_tokenId][_from] >= _amount, "_internalTransferFrom: must own the funds");
}
Expand Down Expand Up @@ -147,8 +147,8 @@ library LibTokenizedVault {
) internal {
AppStorage storage s = LibAppStorage.diamondStorage();

if (s.marketLockedBalances[_from][_tokenId] > 0) {
require(s.tokenBalances[_tokenId][_from] - s.marketLockedBalances[_from][_tokenId] >= _amount, "_internalBurn: tokens for sale in mkt");
if (s.lockedBalances[_from][_tokenId] > 0) {
require(s.tokenBalances[_tokenId][_from] - s.lockedBalances[_from][_tokenId] >= _amount, "_internalBurn: tokens locked");
} else {
require(s.tokenBalances[_tokenId][_from] >= _amount, "_internalBurn: must own the funds");
}
Expand Down Expand Up @@ -292,4 +292,9 @@ library LibTokenizedVault {

_withdrawableDividend = (_withdrawnSoFar >= holderDividend) ? 0 : holderDividend - _withdrawnSoFar;
}

function _getLockedBalance(bytes32 _accountId, bytes32 _tokenId) internal view returns (uint256 amount) {
AppStorage storage s = LibAppStorage.diamondStorage();
return s.lockedBalances[_accountId][_tokenId];
}
}
13 changes: 0 additions & 13 deletions test/T02User.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,4 @@ contract T02UserTest is D03ProtocolDefaults, MockAccounts {
nayms.setEntity(signer1Id, entityId);
assertEq(nayms.getEntity(signer1Id), entityId);
}

function testGetBalanceOfTokensForSale() public {
bytes32 entityId = createTestEntity(account0Id);

// nothing at first
assertEq(nayms.getBalanceOfTokensForSale(entityId, entityId), 0);

nayms.enableEntityTokenization(entityId, "ENTITYSYMBOL");

// now start token sale to create an offer
nayms.startTokenSale(entityId, 100, 100);
assertEq(nayms.getBalanceOfTokensForSale(entityId, entityId), 100);
}
}
30 changes: 28 additions & 2 deletions test/T03TokenizedVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ contract T03TokenizedVaultTest is D03ProtocolDefaults, MockAccounts {
return abi.decode(result, (TradingCommissionsConfig));
}

function testGetLockedBalance() public {
bytes32 entityId = createTestEntity(account0Id);

// nothing at first
assertEq(nayms.getLockedBalance(entityId, entityId), 0);

// now start token sale to create an offer
nayms.enableEntityTokenization(entityId, "Entity1");
nayms.startTokenSale(entityId, 100, 100);

assertEq(nayms.getLockedBalance(entityId, entityId), 100);
}

function testBasisPoints() public {
TradingCommissionsBasisPoints memory bp = nayms.getTradingCommissionsBasisPoints();

Expand Down Expand Up @@ -251,7 +264,7 @@ contract T03TokenizedVaultTest is D03ProtocolDefaults, MockAccounts {
writeTokenBalance(account0, naymsAddress, wethAddress, depositAmount);
nayms.externalDeposit(wethAddress, 1 ether);
vm.prank(account9);
vm.expectRevert("not the entity's admin");
vm.expectRevert("payDividendFromEntity: not the entity's admin");
nayms.payDividendFromEntity(bytes32("0x1"), 1 ether);
}

Expand All @@ -273,6 +286,19 @@ contract T03TokenizedVaultTest is D03ProtocolDefaults, MockAccounts {
assertEq(nayms.internalTokenSupply(acc0EntityId), 0, "Testing when the participation token supply is 0, but par token supply is NOT 0");

bytes32 randomGuid = bytes32("0x1");

address nonAdminAddress = vm.addr(0xACC9);
bytes32 nonAdminId = LibHelpers._getIdForAddress(nonAdminAddress);
nayms.setEntity(nonAdminId, acc0EntityId);

vm.startPrank(nonAdminAddress);
vm.expectRevert("payDividendFromEntity: not the entity's admin");
nayms.payDividendFromEntity(randomGuid, 10 ether);
vm.stopPrank();

vm.expectRevert("payDividendFromEntity: insufficient balance");
nayms.payDividendFromEntity(randomGuid, 10 ether);

nayms.payDividendFromEntity(randomGuid, 1 ether);
// note: When the participation token supply is 0, payDividend() should transfer the payout directly to the payee
assertEq(nayms.internalBalanceOf(acc0EntityId, nWETH), 1 ether, "acc0EntityId nWETH balance should INCREASE (transfer)");
Expand Down Expand Up @@ -400,7 +426,7 @@ contract T03TokenizedVaultTest is D03ProtocolDefaults, MockAccounts {
);

uint256 takerBuyAmount = 1e18;
console2.log(nayms.getBalanceOfTokensForSale(eAlice, eAlice));
console2.log(nayms.getLockedBalance(eAlice, eAlice));

TradingCommissions memory tc = nayms.calculateTradingCommissions(takerBuyAmount);

Expand Down
Loading

0 comments on commit f8b2d59

Please sign in to comment.