Prepared by:
HALBORN
Last Updated 04/25/2024
Date of Engagement by: September 4th, 2022 - September 28th, 2022
97% of all REPORTED Findings have been addressed
All findings
29
Critical
10
High
8
Medium
4
Low
2
Informational
5
Degis engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-09-04 and ending on 2022-09-28. The security assessment was scoped to the smart contracts provided to the Halborn team.
The team at Halborn was provided two weeks for the engagement and assigned two full-time security engineers to audit the security of the smart contract. The security engineers are blockchain and smart-contract security experts with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this audit is to:
Ensure that smart contract functions operate as intended
Identify potential security issues with the smart contracts
In summary, Halborn identified some security risks that were mostly addressed by the Degis team.
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the audit:
Research into architecture and purpose
Smart contract manual code review and walkthrough
Graphing out functionality and contract logic/connectivity/functions (solgraph
)
Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes
Manual testing by custom scripts
Scanning of solidity files for vulnerabilities, security hot-spots or bugs. (MythX
)
Static Analysis of security for scoped contract, and imported functions. (Slither
)
Testnet deployment (Brownie
, Remix IDE
)
IN-SCOPE: The security assessment was scoped on all the contracts present under the src
folder on commit https://github.com/Degis-Insurance/Degis-SCProtection/tree/975aaad510ee3fd50c005eb8bf224be38c08bda0
Critical
10
High
8
Medium
4
Low
2
Informational
5
Impact x Likelihood
HAL-22
HAL-23
HAL-20
HAL-21
HAL-17
HAL-18
HAL-19
HAL-01
HAL-03
HAL-04
HAL-05
HAL-06
HAL-07
HAL-08
HAL-09
HAL-10
HAL-11
HAL-14
HAL-15
HAL-12
HAL-13
HAL-16
HAL-24
HAL-26
HAL-25
HAL-02
HAL-28
HAL-29
HAL-27
Security analysis | Risk level | Remediation Date |
---|---|---|
DEPOSITING TO ANY POOL/TOKEN WITH ANY AMOUNT VIA CONTROLLED POLICY CENTER | Critical | Solved - 09/30/2022 |
CODE NOT CHECKING IF TOKEN IS NOT PRESENT | Critical | Solved - 09/30/2022 |
DEPOSITING ON ANY POOL USING ANY TOKEN | Critical | Solved - 09/30/2022 |
AN ATTACKER CAN WITHDRAW NOT OWNED TOKENS AND STEAL FUNDS | Critical | Solved - 09/30/2022 |
UPDATING THE 0 INDEX TOKEN WEIGHT VIA UNREGISTERED TOKENS | Critical | Solved - 09/30/2022 |
PUBLICLY EXPOSED FUNCTIONS | Critical | Solved - 09/30/2022 |
DEPOSITING TO SECONDARY TOKENS DOES CAUSE THE CONTRACT TO LOCK | Critical | Solved - 09/30/2022 |
POOL INCOMES ON REPORTER REWARD CAN BE ARBITRARILY INCREASED | Critical | Solved - 09/30/2022 |
INVALID VARIABLE VISIBILITY DOES CAUSE CONTRACT DEADLOCK | Critical | Solved - 09/30/2022 |
INVALID EXTERNAL CALL DOES CAUSE CONTRACT DEADLOCK | Critical | Solved - 09/30/2022 |
UPDATE REWARDS MAY CAUSE A DENIAL OF SERVICE BETWEEN YEARS | High | Solved - 09/30/2022 |
BUYING COVER FOR THREE MONTHS IS NEVER COUNTED DURING THE CURRENT MONTH | High | Solved - 10/01/2022 |
MINTING ZERO AMOUNT DEADLOCK IS PRODUCED DURING PAYOUT CLAIMING | High | Solved - 09/30/2022 |
COVER MAY BE UPDATED FOR MONTH'S VALUES OUT OF RANGE | High | Solved - 10/01/2022 |
INVALID REWARD UPDATING MECHANISM | High | Solved - 09/30/2022 |
REPORTING DEADLOCK IF QUORUM NOT REACHED | High | Solved - 09/30/2022 |
INVALID PERCENTAGE RESULTS IN LESS PAYED DEBT | High | Solved - 10/01/2022 |
UNABLE TO CLAIM PAYOUTS | High | Solved - 09/30/2022 |
ANYONE CAN DEPLOY COVER RIGHT TOKENS | Medium | Solved - 09/30/2022 |
CRTOKENS MAY BE MINTED/BURNED ARBITRARILY IF POLICY CENTER IS NOT SET | Medium | Risk Accepted |
SAFEREWARDTRANSFER SHOULD CHECK BEFORE/AFTER BALANCE | Medium | Solved - 09/30/2022 |
OWNER CAN RENOUNCE OWNERSHIP | Medium | Risk Accepted |
MODIFYING DEFAULT POOLS | Low | Solved - 09/30/2022 |
STRANGE CODE BEHAVIOUR | Low | Solved - 10/01/2022 |
INFINITE VOTING BY BYPASSING LOCKING AND RE-CLAIMING LOCKED TOKENS | Informational | - |
TYPO ON FUNCTION DECLARATION | Informational | Solved - 10/01/2022 |
DEPOSIT FUNCTION CAN BE IMPERSONATED | Informational | Solved - 09/30/2022 |
UNNECESSARY CALL | Informational | Acknowledged |
UNNECESSARY CHECK | Informational | Solved - 09/30/2022 |
// Critical
The function setter setPolicyCenter
in charge of modifying the variable policyCenter
, used to verify whether a function's caller is the policy center smart contract or not by checking the address stored in this variable, has no access control. Thus, any account can modify the address stored in policyCenter
which is used to verify the caller in relevant functions such as depositFromPolicyCenter
and withdrawFromPolicyCenter
. This allows the depositFromPolicyCenter
to be called without ever having to transfer tokens. This is causing an attacker to add pool liquidity to any desired pool ID with any chosen token and without any amount without ever owning a single token. This allows the attacker to gain full control on the balance of every single pool on the system.
function setPolicyCenter(address _policyCenter) public {
policyCenter = _policyCenter;
}
function depositFromPolicyCenter(
uint256 _id,
address _token,
uint256 _amount,
address _user
) external {
require(
msg.sender == policyCenter,
"Only policyCenter can call stakedLiquidity"
);
_deposit(_id, _token, _amount, _user);
}
SOLVED: This issue was solved by implementing access control modifiers.
// Critical
The _getIndex
function under WeightedFarmingPool
does not check if the token was not found and returns index 0
as default, causing not found tokens to be treated as part of the pool tokens.
function _getIndex(uint256 _id, address _token)
internal
view
returns (uint256 index)
{
address[] memory allTokens = pools[_id].tokens;
uint256 length = allTokens.length;
for (uint256 i; i < length; ) {
if (allTokens[i] == _token) {
index = i;
break;
} else {
unchecked {
++i;
}
}
}
}
SOLVED: Instead of relying on arrays, a new mapping mapping(bytes32 => bool) public supported;
was introduced to keep track of supported tokens for a given pool id.
// Critical
The deposit
function under WeightedFarmingPool
does accept any token as parameter. This token is then compared against all pool tokens and the index returned. However, since the _getIndex
function does return 0
when a token is not found, the first token of the pool will be used and balance incremented. Furthermore, since the system does not implement any token white-listing a malicious token can be used so the transferFrom
function does not perform any real transfer allowing to increment the stacked amount of the first pool token as wished.
uint256 index = _getIndex(_id, _token);
// check if current index exists for user
if (user.amount.length < index + 1) {
user.amount.push(0);
}
if (pool.amount.length < index + 1) {
pool.amount.push(0);
}
user.amount[index] += _amount;
user.share += _amount * pool.weight[index];
pool.amount[index] += _amount;
pool.shares += _amount * pool.weight[index];
user.rewardDebt = (user.share * pool.accRewardPerShare) / SCALE;
SOLVED: Instead of relying on arrays, a new mapping mapping(bytes32 => bool) public supported;
was introduced to keep track of supported tokens for a given pool id.
// Critical
The _withdraw
function is using the invalid implemented _getIndex
which is causing the transfer
to take place into any user controlled token from the parameters.
function _withdraw(
uint256 _id,
address _token,
uint256 _amount,
address _user
) internal {
require(_amount > 0, "Zero amount");
require(_id <= counter, "Pool not exists");
updatePool(_id);
PoolInfo storage pool = pools[_id];
UserInfo storage user = users[_id][_user];
if (user.share > 0) {
uint256 pending = (user.share * pool.accRewardPerShare) /
SCALE -
user.rewardDebt;
uint256 actualReward = _safeRewardTransfer(
pool.rewardToken,
_user,
pending
);
emit Harvest(_id, _user, _user, actualReward);
}
IERC20(_token).transfer(_user, _amount);
SOLVED: Instead of relying on arrays, a new mapping mapping(bytes32 => bool) public supported;
was introduced to keep track of supported tokens for a given pool id.
// Critical
The updateWeight
function is updating the pool weight via the _getIndex
index, which as stated before it is incorrectly implemented. This allows the first token weight on the pool to be updated if an unregistered or invalid token is provided as the parameter. Furthermore, the code does not check if the weight is greater than 0.
function updateWeight(
uint256 _id,
address _token,
uint256 _newWeight
) external {
updatePool(_id);
uint256 index = _getIndex(_id, _token);
pools[_id].weight[index] = _newWeight;
}
SOLVED: Instead of relying on arrays, a new mapping mapping(bytes32 => bool) public supported;
was introduced to keep track of supported tokens for a given pool id.
// Critical
Several functions are being involved in the management of the WeightedFarmingPool
contract, which are publicly exposed and without any sort of access control:
addPool
: It can add an arbitrary pool into the system.addToken
: It can add any token, even not whitelisted, into any pool.updateWeight
: It does allow updating the weight of any token of any pool.setWeight
: Same as updateWeight
but for the entire pool tokens.updateRewardSpeed
: It does allow updating the rewards per second for a pool.setPolicyCenter
: as described on "DEPOSITING TO ANY POOL/TOKEN WITH ANY AMOUNT VIA CONTROLLED POLICY CENTER".function updateWeight(
uint256 _id,
address _token,
uint256 _newWeight
) external {
updatePool(_id);
uint256 index = _getIndex(_id, _token);
pools[_id].weight[index] = _newWeight;
}
SOLVED: All the stated calls are checking if the sender is a valid-registered pool using IPriorityPoolFactory
. The setPolicyCenter
function was removed from the WeightedFarmingPool
contract.
// Critical
The code under _deposit
is assuming that user.amount
will contain an array for all newly added tokens to the pool, which is not the case if the user performs a deposit for the first time on a token whose index is !=0
.
// check if current index exists for user
if (user.amount.length < index + 1) {
user.amount.push(0);
}
if (pool.amount.length < index + 1) {
pool.amount.push(0);
}
user.amount[index] += _amount;
user.share += _amount * pool.weight[index];
pool.amount[index] += _amount;
pool.shares += _amount * pool.weight[index];
user.rewardDebt = (user.share * pool.accRewardPerShare) / SCALE;
SOLVED: The code is now checking the user.amount
length and comparing it against the extracted index
. If it is required, empty values will be pushed to satisfy the difference between them.
// Critical
The function premiumIncome
allows increasing arbitrarily the array of pool incomes used to calculate the final reward for a correct reporter. Using this function to increase the value of a selected pool income before executing an incident report made from a malicious account could lead to an attacker draining the entire SHD
token balance from Treasury
smart contract.
function premiumIncome(uint256 _poolId, uint256 _amount) external {
poolIncome[_poolId] += _amount;
}
SOLVED: The code is now checking that the caller is only the policyCenter
.
// Critical
When the function dynamicPremiumRatio
is executed 7 days after the pool's deployment, this function tries to retrieve dynamicPoolCounter
from PriorityPoolFactory
during the dynamic ratio calculation. Since the visibility of this variable is internal
, the function always revert at this point.
if (fromStart > 7 days) {
// Covered ratio = Covered amount of this pool / Total covered amount
uint256 coveredRatio = ((activeCovered() + _coverAmount) * SCALE) /
(ISimpleProtectionPool(protectionPool).getTotalCovered() +
_coverAmount);
address lp = currentLPAddress();
// LP Token ratio = LP token in this pool / Total lp token
uint256 tokenRatio = (SimpleERC20(lp).totalSupply() * SCALE) /
SimpleERC20(protectionPool).totalSupply();
// Total dynamic pools
uint256 numofPools = IFactory(priorityPoolFactory)
.dynamicPoolCounter();
// Dynamic premium ratio
// ( N = total dynamic pools ≤ total pools )
//
// Covered 1
// --------------- + -----
// TotalCovered N
// dynamic ratio = -------------------------- * base ratio
// LP Amount 1
// ----------------- + -----
// Total LP Amount N
//
ratio =
(basePremiumRatio * (coveredRatio * numofPools + SCALE)) /
((tokenRatio * numofPools) + SCALE);
// Total max capacity
uint256 public totalMaxCapacity;
// Whether a pool is already dynamic
mapping(address => bool) public dynamic;
uint256 internal dynamicPoolCounter;
// Record whether a protocol token or pool address has been registered
mapping(address => bool) public poolRegistered;
mapping(address => bool) public tokenRegistered;
SOLVED: The dynamicPoolCount
variable has been made public
for external usages.
// Critical
During the payout claiming process, some crTokens
are burned in the claim
function located at PayoutPool
smart contract. Since the onlyPolicyCenter
modifier is applied to the burn
function, the smart contract associated to the address stored in policyCenter
is the only one that can execute this function. Therefore, claim
function always reverts at this point.
uint256 claimableBalance = ICoverRightToken(_crToken).getClaimableOf(
_user
);
uint256 claimable = (claimableBalance * payout.ratio) / SCALE;
uint256 coverIndex = IPriorityPool(payout.priorityPool).coverIndex();
claimed = (claimable * coverIndex) / 10000;
ICoverRightToken(_crToken).burn(
_poolId,
_user,
// burns the users' crToken balance, not the payout amount,
// since rest of the payout will be minted as a new generation token
claimableBalance
);
function burn(
uint256 _poolId,
address _user,
uint256 _amount
) external onlyPolicyCenter nonReentrant {
require(_amount > 0, "Zero Amount");
require(_poolId == POOL_ID, "Wrong pool id");
_burn(_user, _amount);
}
SOLVED: The CoverRightToken
smart contract now implements a modifier to identify whether a call has been made from PolicyCenter
or PayoutPool
smart contracts.
// High
During the calculation of the difference of months between reward's updates, changes of years are not taken into consideration. Since lastRewardTimestamp
can be a high month's value, after a new year the month's counter starts from 1
resulting in an integer overflow as soon as currentM - lastM
is evaluated due this calculation is not considering the years elapsed between updates.
function _updateReward(uint256 _id)
internal
view
returns (uint256 totalReward)
{
PoolInfo storage pool = pools[_id];
uint256 currentTime = block.timestamp;
uint256 lastRewardTime = pool.lastRewardTimestamp;
(uint256 lastY, uint256 lastM, uint256 lastD) = lastRewardTime
.timestampToDate();
(uint256 currentY, uint256 currentM, ) = currentTime.timestampToDate();
uint256 monthPassed = currentM - lastM;
function _updateReward() internal {
uint256 currentTime = block.timestamp;
// Last reward year & month & day
(uint256 lastY, uint256 lastM, uint256 lastD) = lastRewardTimestamp
.timestampToDate();
// Current year & month & day
(uint256 currentY, uint256 currentM, ) = currentTime.timestampToDate();
uint256 monthPassed = currentM - lastM;
SOLVED: In WeightedFarmingPool.sol
the issue has been fixed by taking into consideration elapsed years between executions. In the case of ProtectionPool.sol
, the function where the issue was located has been removed.
// High
When a cover is bought, the function _updateCoverInfo
is executed in the target pool, this function is responsible for storing the covered amount per month. In the case of trying to cover an amount for three months, this value will only be stored in the index currentMonth + 3
of currentYear
. The main issue comes when the function activeCovered
tries to retrieve the active cover amount, since this function iterates over coverInMonth
array starting from currentMonth
as index 0
, the third month is never reached due the loop condition is i < 3
.
(uint256 currentYear, uint256 currentMonth, ) = block
.timestamp
.timestampToDate();
uint256 endMonth = currentMonth + _length;
// ! Remove redundant counts
// ! Previously it is counted in multiple months
// for (uint256 i; i < _length; ) {
coverInMonth[currentYear][endMonth] += _amount;
(uint256 currentYear, uint256 currentMonth, ) = block
.timestamp
.timestampToDate();
// Only count the latest 3 months
for (uint256 i; i < 3; ) {
covered += coverInMonth[currentYear][currentMonth];
SOLVED: Now _updateCoverInfo
stores cover information into right year and month indexes.
// High
The newPayout
function defines a new payout in the PayoutPool
smart contract, one of the parameters used to calculate a payout is payout.ratio
. This attribute helps to calculate the claimable shield in the payout, as well as the amount of crTokens
to mint in the next generation. The issue lies when payout.ratio
is equal to the constant SCALE
, this happens when _amount
and the return value from activeCovered
function is the same during the execution of _retrievePayout
, causing claimableBalance
and claimable
variables to be equal and setting newGenerationCRAmount
variable to 0
. Considering that newGenerationCRAmount
is the variable being used to specify the number of tokens to mint in the next generation, the function always reverts at this point.
(uint256 claimed, uint256 newGenerationCRAmount) = IPayoutPool(
payoutPool
).claim(msg.sender, _crToken, _poolId, _generation);
emit PayoutClaimed(msg.sender, claimed);
uint256 expiry = ICoverRightToken(_crToken).expiry();
// Check if the new generation crToken has been deployed
// If so, get the address
// If not, deploy the new generation cr token
address newCRToken = _checkNewCRToken(
_poolId,
poolName,
expiry,
_generation++
);
ICoverRightToken(newCRToken).mint(
_poolId,
msg.sender,
newGenerationCRAmount
);
uint256 claimableBalance = ICoverRightToken(_crToken).getClaimableOf(
_user
);
uint256 claimable = (claimableBalance * payout.ratio) / SCALE;
uint256 coverIndex = IPriorityPool(payout.priorityPool).coverIndex();
claimed = (claimable * coverIndex) / 10000;
ICoverRightToken(_crToken).burn(
_poolId,
_user,
// burns the users' crToken balance, not the payout amount,
// since rest of the payout will be minted as a new generation token
claimableBalance
);
SimpleIERC20(shield).transfer(_user, claimed);
// Amount of new generation cr token to be minted
newGenerationCRAmount = claimableBalance - claimable;
uint256 payoutRatio;
activeCovered() > 0
? payoutRatio = (_amount * SCALE) / activeCovered()
: payoutRatio = 0;
IPayoutPool(payoutPool).newPayout(
poolId,
generation,
_amount,
payoutRatio,
address(this)
);
SOLVED: The claimPayout
function where reverts were produced now verifies if newGenerationCRAmount
is equal to 0
to prevent minting 0
tokens.
// High
When a new deadline is updated, the resulting month may be out of range due the function is not considering this value exceeding the highest month's value (12
) which would lead to a change of year. As a consequence, new covers in this situation could never be obtained from coverInMonth
since they would have been written out of months' range.
(uint256 currentYear, uint256 currentMonth, ) = block
.timestamp
.timestampToDate();
uint256 endMonth = currentMonth + _length;
// ! Remove redundant counts
// ! Previously it is counted in multiple months
// for (uint256 i; i < _length; ) {
coverInMonth[currentYear][endMonth] += _amount;
SOLVED: Now _updateCoverInfo
considers elapsed time between years by increasing currentYear
variable when is required and controlling endMonth
variable bounces.
// High
The _updateReward
in case of multiple months passed is not properly implemented/handled:
monthPassed
should also consider if currentY
and lastY
are different and add the difference to monthPassed
multiplied by 12. Otherwise, the for
loop will only iterate over a maximum of 12 months and iterate 0 times if the date is the same month 2 years apart.
On iteration index 0: The _getDaysInMonth
should be used to fill the lastD
on endTimestamp
. Otherwise, it is only computing the seconds that passed from the last update until that same day at midnight.
On iteration i == monthPassed
the lastM
should be using currentM
instead or lastM + i
For any other iteration the lastM
should be used with lastM + i
including the fetching of _getDaysInMonth
and the speed
for (uint256 i; i < monthPassed + 1; ) {
// First month reward
if (i == 0) {
// End timestamp of the first month
uint256 endTimestamp = DateTimeLibrary
.timestampFromDateTime(lastY, lastM, lastD, 23, 59, 59);
totalReward +=
(endTimestamp - lastRewardTime) *
speed[_id][lastY][lastM];
}
// Last month reward
else if (i == monthPassed) {
uint256 startTimestamp = DateTimeLibrary
.timestampFromDateTime(lastY, lastM, 1, 0, 0, 0);
totalReward +=
(currentTime - startTimestamp) *
speed[_id][lastY][lastM];
}
// Middle month reward
else {
uint256 daysInMonth = DateTimeLibrary._getDaysInMonth(
lastY,
lastM
);
totalReward +=
(DateTimeLibrary.SECONDS_PER_DAY * daysInMonth) *
speed[_id][lastY][lastM];
}
unchecked {
if (++lastM > 12) {
++lastY;
lastM = 1;
}
++i;
}
}
SOLVED: The code is now implementing all recommended fixes described under the description section.
// High
The settle
function under IncidentReport
does not reset the reported
state when the quorum is not reached those causing the contract to not accept new reports for the same poolId
ever again.
if (_checkQuorum(currentReport.numFor + currentReport.numAgainst)) {
// REJECT or TIED: unlock the priority pool & protection pool immediately
if (res != PASS_RESULT) {
uint256 poolId = currentReport.poolId;
_unpausePools(poolId);
reported[poolId] = false;
}
currentReport.result = res;
_settleVotingReward(_id, res);
emit ReportSettled(_id, res);
} else {
currentReport.result = FAILED_RESULT;
// FAILED: unlock the priority pool & protection pool immediately
_unpausePools(currentReport.poolId);
emit ReportFailed(_id);
}
SOLVED: A new function was added _setReportedStatus
that sets the report status. The status is now reset when the quorum is not reached as well.
// High
The payDebt
function under IncidentReport
is using an invalid numeric value to perform debt percentage calculations. The DEBT_RATIO
is stated as uint256 constant DEBT_RATIO = 80; // 80% as the debt to unlock veDEG
while the actual used value corresponds to 0.8%
instead of the expected 80%
.
function payDebt(uint256 _id, address _user) external {
UserVote memory userVote = votes[_user][_id];
uint256 finalResult = reports[_id].result;
if (finalResult == 0) revert IncidentReport__NotSettled();
if (userVote.choice == finalResult || finalResult == TIED_RESULT)
revert IncidentReport__NotWrongChoice();
if (userVote.paid) revert IncidentReport__AlreadyPaid();
uint256 debt = (userVote.amount * DEBT_RATIO) / 10000;
SOLVED: The client stated the following: The debt is paid in the form of "DEG". And here user's voting amount is calculated by "veDEG". The max generation ratio between DEG and veDEG is 100(1 DEG max generate 100veDEG) and we all treat it as this max ratio. So, the 10000 is composited of 100*100.
// High
The getExcludedCoverageOf
and getClaimableOf
can be deadlocked since the voteTimestamp
is used without verification whether the last incident is on vote state or not those returning voteTimestamp
of 0 and causing the _getEOD
to underflow. Furthermore, the report can be created by anyone using the report
function under IncidentPool
which will push to poolReports
and cause the getPoolReportsAmount
under getExcludedCoverageOf
to return that last created one which has the voteTimestamp
value of 0. The only possible scenario where this is allowed is when the reported
of the pool is set to false
, and this is achieved when the report voting does succeed.
This scenario is preventing anyone to claim the payout under PayoutPool
for an already voted report and _crToken
.
function getExcludedCoverageOf(address _user)
public
view
returns (uint256 exclusion)
{
IIncidentReport incident = IIncidentReport(incidentReport);
uint256 reportAmount = incident.getPoolReportsAmount(POOL_ID);
if (reportAmount > 0) {
uint256 latestReportId = incident.poolReports(
POOL_ID,
reportAmount - 1
);
(, , , uint256 voteTimestamp, , , , , , , ) = incident.reports(
latestReportId
);
// Check those bought within 2 days
for (uint256 i; i < EXCLUDE_DAYS; ) {
uint256 date = _getEOD(voteTimestamp - (i * 1 days));
exclusion += coverStartFrom[_user][date];
unchecked {
++i;
}
}
}
}
SOLVED: The code is now checking for valid reports based on the generation, it will exclude all invalid reports and only count for reports with a result of SUCCESS
.
// Medium
Anyone can call deployCRToken
under CoverRightTokenFactory
which does deploy a new CoverRight
Token. On the PolicyCenter
contract, the _getCRTokenAddress
function is used to retrieve this token address back using only the _poolId
, _expiry
and _generation
values. This means that anyone could deploy a future CR token before the buyCover
or claimPayout
on the PolicyCenter
. Furthermore, the deployCRToken
does allow to arbitrary set the poolName
and the tokenName
those allowing manipulating and tricking the users if those values are used on the frontend.
function deployCRToken(
string calldata _poolName,
uint256 _poolId,
string calldata _tokenName,
uint256 _expiry,
uint256 _generation
) external returns (address newCRToken) {
require(_expiry > 0, "Zero expiry date");
bytes32 salt = keccak256(
abi.encodePacked(_poolId, _expiry, _generation)
);
require(!deployed[salt], "already deployed");
deployed[salt] = true;
bytes memory bytecode = _getCRTokenBytecode(
_poolName,
_poolId,
_tokenName,
_expiry,
_generation
);
newCRToken = _deploy(bytecode, salt);
saltToAddress[salt] = newCRToken;
emit NewCRTokenDeployed(
_poolId,
_tokenName,
_expiry,
_generation,
newCRToken
);
}
SOLVED: The code is now verifying that calls are only made though the policyCenter
.
// Medium
The modifier onlyPolicyCenter
allows the execution of a function making use of this modifier without reverting the transaction when policyCenter
is not set or equals to 0
. Since this modifier is used to verify the access to critical token's functions as mint
and destroy
, a malicious actor may mint or destroy arbitrarily crTokens
of a specified priority pool in the case that policyCenter
has not been set previously.
modifier onlyPolicyCenter() {
if (policyCenter != address(0)) {
require(msg.sender == policyCenter, "Only policy center");
}
_;
}
RISK ACCEPTED: The Degis team
accepted the risk of this finding.
// Medium
Since the system does implement any token white-listing, the _safeRewardTransfer
function should verify that the difference between the before and after balance corresponds to the actual requested amount
.
function _safeRewardTransfer(
address _token,
address _to,
uint256 _amount
) internal returns (uint256 actualAmount) {
uint256 balance = IERC20(_token).balanceOf(address(this));
if (_amount > balance) {
actualAmount = balance;
} else {
actualAmount = _amount;
}
IERC20(_token).safeTransfer(_to, actualAmount);
}
SOLVED: The code is now verifying the before and after balance and returning that as the actual amount.
// Medium
The OwnableWithoutContext
does contain the renounceOwnership
function, which can be called by the current owner. Calling this function does leave the contract without an owner, denying the possibility to perform any further administrative operations.
function premiumIncome(uint256 _poolId, uint256 _amount) external {
poolIncome[_poolId] += _amount;
}
RISK ACCEPTED: The Degis team
accepted the risk of this finding.
// Low
The storePoolInformation
function under storePoolInformation
does allow replacing the pool ID of 0, overriding the _protectionPool
and shield
tokens.
function storePoolInformation(
address _pool,
address _token,
uint256 _poolId
) external {
// @audit-issue This allows replacing any pool, including the ID 0 with
// _protectionPool and shield
require(msg.sender == priorityPoolFactory, "Only factory can store");
tokenByPoolId[_poolId] = _token;
priorityPools[_poolId] = _pool;
_approvePoolToken(_token);
}
SOLVED: It is verifying using an assert that the protection pool information is never altered.
// Low
The updateRewardSpeed
function under WeightedFarmingPool
does check for years.length == months.length
which does not comply with the logic of the code on this context.
function updateRewardSpeed(
uint256 _id,
uint256 _newSpeed,
uint256[] memory _years,
uint256[] memory _months
) external {
require(_years.length == _months.length, "Wrong length");
uint256 length = _years.length;
for (uint256 i; i < length; ) {
speed[_id][_years[i]][_months[i]] += _newSpeed;
unchecked {
++i;
}
}
}
SOLVED: The _years
argument used by the function is to indicate the year for the _months
array, and values can be repeated.
// Informational
// Informational
The setMaxCapacity
under PriorityPool
will always revert as it is calling updateMaxCapacity
but the declared selector on PriorityPoolFactory
is updateMaxCapaity
.
function updateMaxCapaity(bool _isUp, uint256 _diff)
external
onlyPriorityPool
{
if (_isUp) {
totalMaxCapacity += _diff;
} else totalMaxCapacity -= _diff;
emit MaxCapacityUpdated(totalMaxCapacity);
}
function setMaxCapacity(bool _isUp, uint256 _maxCapacity) external {
require(msg.sender == owner);
maxCapacity = _maxCapacity;
uint256 diff;
if (_isUp) {
diff = _maxCapacity - maxCapacity;
} else {
diff = maxCapacity - _maxCapacity;
}
IFactory(priorityPoolFactory).updateMaxCapacity(_isUp, diff);
}
SOLVED: The typo was fixed, and the interface is updated to updateMaxCapacity
.
// Informational
During the execution of deposit
, the main logic is delegated to an internal function named _deposit
. This function makes use of the parameter _user
to define the address which executed the deposit. Since this function does not verify which account is executing it, any account could execute deposits on behalf of other accounts.
function deposit(
uint256 _id,
address _token,
uint256 _amount,
address _user
) external {
_deposit(_id, _token, _amount, _user);
IERC20(_token).transferFrom(msg.sender, address(this), _amount);
}
SOLVED: The code is not checking for msg.sender
, only deposits.
// Informational
The _unpausePools
is not needed on closeReport
since the _pausePools
is only called on startVoting
. Once startVoting
is called, which sets the currentReport.status = VOTING_STATUS
, the closeReport
can not be called due to the currentReport.status != PENDING_STATUS
check.
function closeReport(uint256 _id) external onlyOwner {
Report storage currentReport = reports[_id];
if (currentReport.status != PENDING_STATUS)
revert IncidentReport__WrongStatus();
// Must close the report before pending period ends
if (_passedPendingPeriod(currentReport.reportTimestamp))
revert IncidentReport__WrongPeriod();
currentReport.status = CLOSE_STATUS;
uint256 poolId = currentReport.poolId;
reported[poolId] = false;
_unpausePools(poolId);
emit ReportClosed(_id, block.timestamp);
}
ACKNOWLEDGED
// Informational
The settle
function under OnboardProposal
does check if the proposal.result
is greater than zero and then reverts. However, that condition will never be reachable as the status
condition will always proceed first and always revert if the settle
function was ever called before.
function settle(uint256 _id) external {
Proposal storage proposal = proposals[_id];
if (proposal.status != VOTING_STATUS)
revert OnboardProposal__WrongStatus();
if (!_passedVotingPeriod(proposal.voteTimestamp))
revert OnboardProposal__WrongPeriod();
if (proposal.result > 0) revert OnboardProposal__AlreadySettled();
// If reached quorum, settle the result
if (_checkQuorum(proposal.numFor + proposal.numAgainst)) {
uint256 res = _getVotingResult(
proposal.numFor,
proposal.numAgainst
);
// If this proposal not passed, allow new proposals for the same project
// If it passed, not allow the same proposals
if (res != PASS_RESULT) {
// Allow for new proposals to be proposed for this protocol
proposed[proposal.protocolToken] = false;
}
proposal.result = res;
proposal.status = SETTLED_STATUS;
emit ProposalSettled(_id, res);
}
// Else, set the result as "FAILED"
else {
proposal.result = FAILED_RESULT;
proposal.status = SETTLED_STATUS;
// Allow for new proposals to be proposed for this protocol
proposed[proposal.protocolToken] = false;
emit ProposalFailed(_id);
}
}
SOLVED: The check was removed
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed