Skip to content

Commit

Permalink
Double locked balance (#81)
Browse files Browse the repository at this point in the history
* fix: double balance lock

* fix: add missing helper method

* refactor: double balance lock test cleanup

* feat: add usdc to defaults

* chore: cleanup the test

* fix: policy validation test

* fix: properly check for utilized capacity

* chore: remove changePrank calls from 50CR test
  • Loading branch information
amarinkovic authored Oct 20, 2023
1 parent 2e78123 commit c916abb
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 56 deletions.
6 changes: 4 additions & 2 deletions src/diamonds/nayms/libs/LibEntity.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,16 @@ library LibEntity {
require(simplePolicy.asset == entity.assetId, "asset not matching with entity");

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

// 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");

// The entity's balance must be >= to the updated capacity requirement
require(LibTokenizedVault._internalBalanceOf(_entityId, simplePolicy.asset) >= updatedUtilizedCapacity, "not enough capital");
uint256 availableBalance = LibTokenizedVault._internalBalanceOf(_entityId, simplePolicy.asset) - LibTokenizedVault._getLockedBalance(_entityId, simplePolicy.asset);
require(availableBalance >= additionalCapacityNeeded, "not enough capital");

require(simplePolicy.startDate >= block.timestamp, "start date < block.timestamp");
require(simplePolicy.maturationDate > simplePolicy.startDate, "start date > maturation date");
Expand Down
66 changes: 37 additions & 29 deletions test/T04Entity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -375,14 +375,17 @@ contract T04EntityTest is D03ProtocolDefaults {
function testCreateSimplePolicyValidation() public {
nayms.createEntity(entityId1, account0Id, initEntity(wethId, LC.BP_FACTOR, LC.BP_FACTOR, false), "entity test hash");

changePrank(su.addr);
// enable simple policy creation
vm.startPrank(su.addr);
vm.expectRevert("simple policy creation disabled");
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);
changePrank(sm.addr);
vm.stopPrank();

// enable simple policy creation
vm.startPrank(sm.addr);
nayms.updateEntity(entityId1, initEntity(wethId, LC.BP_FACTOR, LC.BP_FACTOR, true));
changePrank(su.addr);
vm.stopPrank();

vm.startPrank(su.addr);
// stakeholders entity ids array different length to signatures array
bytes[] memory sig = stakeholders.signatures;
stakeholders.signatures = new bytes[](0);
Expand All @@ -407,43 +410,44 @@ contract T04EntityTest is D03ProtocolDefaults {
vm.expectRevert("external token is not supported");
simplePolicy.asset = LibHelpers._getIdForAddress(wbtcAddress);
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);
vm.stopPrank();

changePrank(sa.addr);
vm.startPrank(sa.addr);
nayms.addSupportedExternalToken(wbtcAddress);
simplePolicy.asset = wbtcId;
changePrank(su.addr);
vm.startPrank(su.addr);
vm.expectRevert("asset not matching with entity");
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);
simplePolicy.asset = wethId;

// test caller is not system underwriter
changePrank(account9);
vm.startPrank(account9);
vm.expectRevert(abi.encodeWithSelector(InvalidGroupPrivilege.selector, account9._getIdForAddress(), systemContext, "", LC.GROUP_SYSTEM_UNDERWRITERS));
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);

changePrank(su.addr);
vm.startPrank(su.addr);
// test capacity
vm.expectRevert("not enough available capacity");
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);

changePrank(sm.addr);
vm.startPrank(sm.addr);
// update max capacity
nayms.updateEntity(entityId1, initEntity(wethId, 5000, 300000, true));
changePrank(su.addr);
vm.startPrank(su.addr);
// test collateral ratio constraint
vm.expectRevert("not enough capital");
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);

changePrank(sm.addr);
vm.startPrank(sm.addr);
// fund the policy sponsor entity
nayms.updateEntity(entityId1, initEntity(wethId, 5000, 300000, true));
changePrank(account0);
vm.startPrank(account0);
writeTokenBalance(account0, naymsAddress, wethAddress, 100000);
assertEq(weth.balanceOf(account0), 100000);
nayms.externalDeposit(wethAddress, 100000);
assertEq(nayms.internalBalanceOf(entityId1, wethId), 100000);

changePrank(su.addr);
vm.startPrank(su.addr);

// start date too early
uint256 blockTimestampBeforeWarp;
Expand Down Expand Up @@ -476,19 +480,19 @@ contract T04EntityTest is D03ProtocolDefaults {
bytes32[] memory r;
uint16[] memory bp;

changePrank(systemAdmin);
vm.startPrank(systemAdmin);
nayms.addFeeSchedule(LC.DEFAULT_FEE_SCHEDULE, LC.FEE_TYPE_PREMIUM, r, bp);
changePrank(su.addr);
vm.startPrank(su.addr);
vm.expectRevert("must have fee schedule receivers");
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);

// add back fee receiver
r = b32Array1(NAYMS_LTD_IDENTIFIER);
bp = u16Array1(300);
changePrank(systemAdmin);
vm.startPrank(systemAdmin);
nayms.addFeeSchedule(LC.DEFAULT_FEE_SCHEDULE, LC.FEE_TYPE_PREMIUM, r, bp);

changePrank(su.addr);
vm.startPrank(su.addr);
vm.expectRevert("number of commissions don't match");
bytes32[] memory commissionReceiversOrig = simplePolicy.commissionReceivers;
simplePolicy.commissionReceivers = new bytes32[](0);
Expand Down Expand Up @@ -536,6 +540,8 @@ contract T04EntityTest is D03ProtocolDefaults {
assertEq(simplePolicyInfo.claimsPaid, simplePolicy.claimsPaid, "Claims paid amounts should match");
assertEq(simplePolicyInfo.premiumsPaid, simplePolicy.premiumsPaid, "Premiums paid amounts should match");

nayms.cancelSimplePolicy(policyId1);

bytes32[] memory roles = new bytes32[](2);
roles[0] = LibHelpers._stringToBytes32(LC.ROLE_UNDERWRITER);
roles[1] = LibHelpers._stringToBytes32(LC.ROLE_BROKER);
Expand Down Expand Up @@ -719,12 +725,16 @@ contract T04EntityTest is D03ProtocolDefaults {

function testSimplePolicyEntityCapitalUtilization50CR() public {
getReadyToCreatePolicies();
changePrank(sa.addr);
vm.stopPrank();
vm.startPrank(sa.addr);
nayms.assignRole(em.id, systemContext, LC.ROLE_ENTITY_MANAGER);
changePrank(em.addr);
vm.stopPrank();

vm.startPrank(em.addr);
nayms.assignRole(nayms.getEntity(account0Id), nayms.getEntity(account0Id), LC.ROLE_ENTITY_COMPTROLLER_COMBINED);
vm.stopPrank();

changePrank(su.addr);
vm.startPrank(su.addr);
(stakeholders, simplePolicy) = initPolicyWithLimit(testPolicyDataHash, 42002);
vm.expectRevert("not enough capital");
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);
Expand All @@ -733,8 +743,8 @@ contract T04EntityTest is D03ProtocolDefaults {
(stakeholders, simplePolicy) = initPolicyWithLimit(testPolicyDataHash, 42000);
nayms.createSimplePolicy(policyId1, entityId1, stakeholders, simplePolicy, testPolicyDataHash);
assertEq(nayms.getLockedBalance(entityId1, simplePolicy.asset), 21000, "locked balance should INCREASE");
vm.stopPrank();

changePrank(account0);
vm.expectRevert("_internalTransfer: insufficient balance available, funds locked");
nayms.paySimpleClaim(LibHelpers._stringToBytes32("claimId"), policyId1, DEFAULT_INSURED_PARTY_ENTITY_ID, 2);

Expand All @@ -747,20 +757,20 @@ contract T04EntityTest is D03ProtocolDefaults {
assertEq(nayms.getEntityInfo(entityId1).utilizedCapacity, 21000 - 1, "entity utilization should DECREASE when a claim is made");
assertEq(nayms.getLockedBalance(entityId1, simplePolicy.asset), 21000 - 1, "entity locked balance should DECREASE");

changePrank(account0);
writeTokenBalance(account0, naymsAddress, wethAddress, 200_000);
nayms.externalDeposit(wethAddress, 200_000);
assertEq(nayms.internalBalanceOf(entityId1, simplePolicy.asset), 20999 + 200_000, "after deposit, entity balance of nWETH should INCREASE");

// increase max cap from 30_000 to 221_000
Entity memory newEInfo = nayms.getEntityInfo(entityId1);
newEInfo.maxCapacity = 221_000;
changePrank(sm.addr);
vm.startPrank(sm.addr);
nayms.updateEntity(entityId1, newEInfo);
vm.stopPrank();

bytes32 policyId2 = LibHelpers._stringToBytes32("policyId2");

changePrank(su.addr);
vm.startPrank(su.addr);
(stakeholders, simplePolicy) = initPolicyWithLimit(testPolicyDataHash, 400_003);
vm.expectRevert("not enough capital");
nayms.createSimplePolicy(policyId2, entityId1, stakeholders, simplePolicy, testPolicyDataHash);
Expand All @@ -769,29 +779,27 @@ contract T04EntityTest is D03ProtocolDefaults {
// note: brings us to 100% max capacity
nayms.createSimplePolicy(policyId2, entityId1, stakeholders, simplePolicy, testPolicyDataHash);
assertEq(nayms.getLockedBalance(entityId1, simplePolicy.asset), 21000 - 1 + 200_000, "locked balance should INCREASE");
vm.stopPrank();

changePrank(account0);
vm.expectRevert("_internalTransfer: insufficient balance available, funds locked");
nayms.paySimpleClaim(LibHelpers._stringToBytes32("claimId2"), policyId1, DEFAULT_INSURED_PARTY_ENTITY_ID, 3);

vm.expectRevert("_internalTransfer: insufficient balance available, funds locked");
nayms.paySimpleClaim(LibHelpers._stringToBytes32("claimId2"), policyId2, DEFAULT_INSURED_PARTY_ENTITY_ID, 3);

changePrank(su.addr);
vm.startPrank(su.addr);
nayms.cancelSimplePolicy(policyId1);
vm.stopPrank();

assertEq(nayms.getLockedBalance(entityId1, simplePolicy.asset), 21000 - 1 + 200_000 - 20999, "after cancelling policy, the locked balance should DECREASE");

changePrank(account0);
vm.expectRevert("_internalBurn: insufficient balance available, funds locked");
nayms.externalWithdrawFromEntity(entityId1, account0, wethAddress, 21_000);

nayms.externalWithdrawFromEntity(entityId1, account0, wethAddress, 21_000 - 1);

vm.expectRevert("_internalTransfer: insufficient balance available, funds locked");
nayms.paySimpleClaim(LibHelpers._stringToBytes32("claimId2"), policyId2, DEFAULT_INSURED_PARTY_ENTITY_ID, 1);

changePrank(account0);
writeTokenBalance(account0, naymsAddress, wethAddress, 1);
nayms.externalDeposit(wethAddress, 1);

Expand Down
69 changes: 68 additions & 1 deletion test/T04Market.t.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import { D03ProtocolDefaults, LibHelpers, LC, c as c } from "./defaults/D03ProtocolDefaults.sol";
import { D03ProtocolDefaults, LibHelpers, LibObject, LC, c } from "./defaults/D03ProtocolDefaults.sol";
import { Vm } from "forge-std/Vm.sol";
import { StdStyle } from "forge-std/Test.sol";
import { MockAccounts } from "./utils/users/MockAccounts.sol";

import { Entity, MarketInfo, FeeSchedule, SimplePolicy, Stakeholders, CalculatedFees } from "src/diamonds/nayms/interfaces/FreeStructs.sol";

import { StdStyle } from "forge-std/StdStyle.sol";

/*
Terminology:
wethId: bytes32 ID of WETH
Expand Down Expand Up @@ -908,4 +910,69 @@ contract T04MarketTest is D03ProtocolDefaults, MockAccounts {
require(price1 < price2, string.concat("best order incorrect: ", vm.toString(price1)));
require(price2 < price3, string.concat("second best order incorrect: ", vm.toString(price2)));
}

function testDoubleLockedBalance_IM24430() public {
/// when creating a policy, available balance is checked to be used for collateral,
/// previously no check was being performed, to see if any part of the balance is locked already
/// attack consists of placing an order to lock the available balance
/// and then creating a policy locking the same funds again

uint256 usdc1000 = 1000e6;
uint256 pToken100 = 100e18;
// uint256 pToken99 = 99e18;

// prettier-ignore
Entity memory entityData = Entity({
assetId: usdcId,
collateralRatio: 5_000,
maxCapacity: 100_000 * 1e6,
utilizedCapacity: 0,
simplePolicyEnabled: true
});

NaymsAccount memory attacker = makeNaymsAcc("Attacker");
NaymsAccount memory userA = makeNaymsAcc("entityA");

vm.startPrank(sm.addr);

// createEntity for attacker
hCreateEntity(attacker.entityId, attacker.id, entityData, "test entity");

// Attacker deposits 1000 USDC + trading fee
fundEntityUsdc(attacker, usdc1000 + 1e7);

vm.startPrank(sm.addr);

// userA startTokenSale with (1000 pToken for 1000.000001 USDC)
hCreateEntity(userA.entityId, userA.id, entityData, "entity test hash");
nayms.enableEntityTokenization(userA.entityId, "E1", "Entity 1 Token");
nayms.startTokenSale(userA.entityId, pToken100, usdc1000 * 2);

/// Attack script
/// place order and lock funds
vm.startPrank(attacker.addr);
nayms.executeLimitOffer(usdcId, usdc1000, userA.entityId, pToken100);

vm.startPrank(su.addr);
/// create policy (double lock?)
uint256 policyLimitAmount = (usdc1000 * 10_000) / entityData.collateralRatio;
(Stakeholders memory stakeholders, SimplePolicy memory simplePolicy) = initPolicyWithLimitAndAsset("offChainHash", policyLimitAmount, usdcId);

vm.expectRevert("not enough capital");
nayms.createSimplePolicy(bytes32("1"), attacker.entityId, stakeholders, simplePolicy, "offChainHash");

// uint256 lockedBalance = nayms.getLockedBalance(attacker.entityId, usdcId);
// uint256 internalBalance = nayms.internalBalanceOf(attacker.entityId, usdcId);
// require(lockedBalance <= internalBalance, "double lock balance attack successful");
}

function fundEntityUsdc(NaymsAccount memory acc, uint256 amount) private {
deal(usdcAddress, acc.addr, amount);
changePrank(acc.addr);
usdc.approve(address(nayms), amount);
uint256 balanceBefore = nayms.internalBalanceOf(acc.entityId, usdcId);
nayms.externalDeposit(usdcAddress, amount);
uint256 balanceAfter = nayms.internalBalanceOf(acc.entityId, usdcId);
assertEq(balanceAfter, balanceBefore + amount, "entity's weth balance is incorrect");
}
}
3 changes: 3 additions & 0 deletions test/defaults/D01Deployment.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ abstract contract D01Deployment is D00GlobalDefaults, DeploymentHelpers {

function makeNaymsAcc(string memory name) public returns (NaymsAccount memory) {
(address addr, uint256 privateKey) = makeAddrAndKey(name);
vm.label(addr, name);
return NaymsAccount({ id: LibHelpers._getIdForAddress(addr), entityId: keccak256(bytes(name)), pk: privateKey, addr: addr });
}

Expand All @@ -55,6 +56,7 @@ abstract contract D01Deployment is D00GlobalDefaults, DeploymentHelpers {
}

constructor() payable {
c.log("\n -- D01 Deployment Defaults\n");
c.log("block.chainid", block.chainid);

bool BOOL_FORK_TEST = vm.envOr({ name: "BOOL_FORK_TEST", defaultValue: false });
Expand Down Expand Up @@ -147,6 +149,7 @@ abstract contract D01Deployment is D00GlobalDefaults, DeploymentHelpers {
nayms.updateRoleGroup(LC.ROLE_ENTITY_CP, LC.GROUP_CANCEL_OFFER, true);

nayms.updateRoleGroup(LC.ROLE_ENTITY_CP, LC.GROUP_EXECUTE_LIMIT_OFFER, true);
nayms.updateRoleGroup(LC.ROLE_ENTITY_ADMIN, LC.GROUP_EXECUTE_LIMIT_OFFER, true);

nayms.updateRoleGroup(LC.ROLE_ENTITY_BROKER, LC.GROUP_PAY_SIMPLE_PREMIUM, true);
nayms.updateRoleGroup(LC.ROLE_ENTITY_INSURED, LC.GROUP_PAY_SIMPLE_PREMIUM, true);
Expand Down
Loading

0 comments on commit c916abb

Please sign in to comment.