EnduracoinToken v1.0.5.11052023 smart contract security audit report performed by Callisto Security Audit Department
Commit 017974d036b24e7f97b1bd49cec64215c36db162
- EnduracoinToken.sol
- EnduracoinValue.sol
- ManageApprovers.sol
- StringHelpers.sol
- ChangeRequests.sol
In total, 2 issues were reported, including:
-
1 high severity issue.
-
1 medium severity issue.
-
0 low severity issues.
In total, 2 notes were reported, including:
-
1 minor observation.
-
1 owner privilege.
The functions (transfer
, approve
, transferFrom
, increaseAllowance
) of EnduracoinToken have the modifier requiresMultiSig()
.
This modifier applies to check vote results if pendingChange.addrValueToChange == owner()
. It means that any approver who creates newChangeRequest
with the parameter addressToUse
equal to the owner address will block all tokens functionality until voting is finished.
Adding condition && msg.sender != pendingChange.chgInitiator
does not fix this issue, because msg.sender
will be a user who call one of those functions, so msg.sender != pendingChange.chgInitiator
will be true
and does not affect on pendingChange.addrValueToChange == owner()
.
Also, the modifier requiresMultiSig()
should allow to owner to call those functions only if this call was approved. So condition || opposeChange >= MIN_VOTES_REQUIRED
should be removed.
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinToken.sol#L97-L128
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/ChangeRequests.sol#L69
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinToken.sol#L74
Change requiresMultiSig()
modifier to this one:
modifier requiresMultiSig() {
if(msg.sender == owner()) {
require(forChange >= MIN_VOTES_REQUIRED, "ENDC: Insufficient approvals.");
}
_;
}
The condition:
if((pendingChangeType == uint8(ChangeType.renounceOwnership)) && (forChange < MIN_VOTES_REQUIRED))
{ invalidInput("Invalid fx call to renounce ownership."); }
does not protect against renounce ownership by anyone, because pendingChangeType == uint8(ChangeType.renounceOwnership
will be false
in most time.
Change this condition pendingChangeType == uint8(ChangeType.renounceOwnership
to pendingChangeType != uint8(ChangeType.renounceOwnership
.
50 Billion Enduracoin will be pre-minted to the owner's wallet.
- The modifier
requiresMultiSig
restrictsowner
from calling the function until voting is finished, but allows anybody else to call the function without restriction. Therefore, in the context of the contractEnduracoinValue
it does not make any sense and can be removed or replaced by the modifieronlyApprovers
.
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinValue.sol#L186
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinValue.sol#L200
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinValue.sol#L209
- The
getPendingChangeRequest()
is a view function, so does not requireonlyApprovers
modifier.
-
Owner must be the final approver in voting for
transfer
,approve
,transferFrom
,increaseAllowance
. If the owner votes earlier the change request can't be approved and all other approvers should vote against it to be able to create a new request. It creates an opportunity for a frontrunning attack by a malicious approver, where he can flip his vote just before the owner's vote (so the owner will not be the last approver). -
The functions
transferOwnership()
andrenounceOwnership()
should not be called externally. So better to make internal functions that will be called from this contract. For example:
// override public functions from Ownable contract
function transferOwnership(address newOwner) public override {
revert("Not allowed");
}
function transferOwnership(address newOwner) public override {
revert("Not allowed");
}
In the EnduracoinValue
contract create internal functions and call it from contract instead of transferOwnership()
and renounceOwnership()
// unnecessary checks were removed, because these functions calling from contract after passing all requirements
function transferOwnershipInternal(address newOwner) internal {
removeApprover(owner());
pendingChangeOfAddress[0] = newOwner;
if (approverAddresses[newOwner] != true) { setApprovers(pendingChangeOfAddress); }
_transferOwnership(newOwner);
emit EnduracoinOwnerTransferred(msg.sender, " changed ENDCValueSmartContract owner to : ", newOwner);
}
function renounceOwnershipInternal() internal {
_transferOwnership(address(0));
emit EnduracoinOwnerTransferred(msg.sender, " changed ENDCValueSmartContract owner to : ", address(0));
}
Do the same for Enduracoin
contract:
// override public functions from Ownable contract
function transferOwnership(address newOwner) public override {
revert("Not allowed");
}
function transferOwnership(address newOwner) public override {
revert("Not allowed");
}
// Add internal function and remove unnecessary checks
function transferOwnershipInternal(address newOwner) internal {
_transferOwnership(newOwner);
emit EnduracoinOwnerTransferred(msg.sender, "ENDC: Ownership transferred to: ", newOwner);
}
function renounceOwnershipInternal() internal {
_transferOwnership(address(0));
emit EnduracoinOwnerTransferred(msg.sender, "ENDC: changed ENDCTokenContract owner to : ", address(0));
}
- The function
removeApprover()
inEnduracoinValue
andEnduracoin
contract are internal and calls from contract after passing all required checks. So you should removerequiresMultiSig
modifier and unnecessary checks. For example:
function removeApprover(address approverToRemove) internal {
emit EnduracoinOwnerTransferred(msg.sender, "ENDCV: Ownership transferred to: ", approverToRemove);
require(approverAddresses[approverToRemove], "INFO: ENDCV - The address entered is not in the approvers list.");
_removeApprover(approverToRemove);
}
- The modifier
requiresMultiSig()
has a check(msg.sender == owner() && approverAddresses[msg.sender])
. The conditionapproverAddresses[msg.sender]
will always beTrue
if themsg.sender
is theowner()
address. Therefore the conditionapproverAddresses[msg.sender]
can safely be removed.
- Change request types
setBaseValueAdjustment
, andsetDailyValueGainAdjustment
are unused in the contracts and can be removed.
- Condition
(pendingChangeType != uint8(ChangeType.renounceOwnership
in the functionremoveApprover
always beTrue
, as the contract cannot access the functionremoveApprover()
for the change typeChangeType.renounceOwnership
.
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinValue.sol#L212C12-L212C67
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinToken.sol#L167C11-L167C67
- When a pending change is executed in
EnduracoinValue
contract theexecutionResult
string is set to "Change Opposed!", but even if the change was successfully executed the eventChangeRequestExecutedEvent
would be emitted with the opposed string instead of a success string.
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinValue.sol#L166
- https://github.com/Enduracoin/EnduracoinToken/blob/017974d036b24e7f97b1bd49cec64215c36db162/EnduracoinValue.sol#L174
- Open-source contact.
- The contract should pass a bug bounty after the completion of the security audit.
- Public testing.
- Automated anomaly detection systems. - NOT IMPLEMENTED. A simple anomaly detection algorithm is recommended to be implemented to detect behavior that is atypical compared to normal for this contract. For instance, the contract must halt deposits if a large amount is being withdrawn quickly until the owner or the project community approves further operations.
- Multisig owner account.
- Standard ERC20-related issues. - NOT IMPLEMENTED. Every contract can potentially receive an unintended ERC20-token deposit without the ability to reject it, even if the contract is not intended to receive or hold tokens. As a result, it is recommended to implement a function that will allow extracting any arbitrary number of tokens from the contract.
- Crosschain address collisions. ETH, ETC, CLO, etc. It is possible that a transaction can be sent to the address of your contract at another chain (as a result of a user mistake or some software fault). It is recommended that you deploy a "mock contract" that would allow you to withdraw any tokens from that address or prevent any funds deposits. Note that you can reject transactions of native tokens deposited, but you can not reject the deposits of ERC20 tokens. You can use this source code as a mock contract: extractor contract source code. The address of a new contract deployed using
CREATE (0xf0)
opcode is assigned following this schemekeccak256(rlp([sender, nonce]))
. Therefore you need to use the same address that was originally used at the main chain to deploy the mock contract at a transaction with thenonce
that matches that on the original chain. Example: If you have deployed your main contract with address 0x010101 at your 2021st transaction, then you need to increase your nonce of 0x010101 address to 2020 at the chain where your mock contract will be deployed. Then you can deploy your mock contract with your 2021st transaction, and it will receive the same address as your mainnet contract.
The audited smart contract can't be deployed. Pointed issues should be fixed before deployment.
It is recommended to adhere to the security practices described in pt. 4 of this report to ensure the contract's operability and prevent any issues that are not directly related to the code of this smart contract.
- v1: https://gist.github.com/yuriy77k/d374031c58202d6b1d57086dfecfc35c
- v2: https://gist.github.com/yuriy77k/af105a36bb8109bcafe742825efb4906
- v3: https://gist.github.com/yuriy77k/b84d523240684ca6c4e1d06fdad525c7
- v4: https://gist.github.com/yuriy77k/5890f3eea29767622baa41dc857620bd
- v1.0.5.10152023: https://gist.github.com/yuriy77k/b58e28c2c667819dd8eab6ff6da0ad6c