Giter Club home page Giter Club logo

2023-01-ondo-findings's Introduction

Ondo Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Contest findings are submitted to this repo

As a sponsor, you have three critical tasks in the contest process:

  1. Weigh in on severity.
  2. Respond to issues.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Please note: because wardens submit issues without seeing each other's submissions, there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the severity label intact and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. It is possible for issues to be considered 0 (Non-critical).

Feel free to use the question label for anything you would like additional C4 input on.

Please don't change the severity labels; that's up to the judge's discretion.

Respond to issues

Label each open/primary High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue. Note that when the repo is public, after all issues are mitigated, wardens will read these comments.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: all warden submissions in these three categories should now be submitted as bulk listings of issues and recommendations:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, we ask that you:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the ok.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

As you undertake that process, we request that you take the following steps:

  1. Within your own GitHub repo, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. Do not close the issue; simply label it as resolved. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-01-ondo-findings's People

Contributors

code423n4 avatar c4-judge avatar kartoonjoy avatar

Watchers

Ashok avatar  avatar

2023-01-ondo-findings's Issues

QA Report

See the markdown file with the details of this report here.

feeRecipient and assetRecipient can be set to the 0 address

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L452
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L465

Vulnerability details

Impact

If the contract manager accidentally sets one or both of these addresses to the zero address, collateral will be burnt.

Proof of Concept

These calls will send the collateral to the zero address: https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L214-L219. In the constructor there are checks to prevent either of these addresses from being set to 0: https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L147-L154

Tools Used

None

Recommended Mitigation Steps

In the setter functions, make sure the input is not the zero address to prevent the contract manager from ever making this mistake.

QA Report

See the markdown file with the details of this report here.

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a ๐Ÿ‘ emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

EpochDuration set to zero prevents all operations that need epoch updates

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L546-L552
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L576-L587

Vulnerability details

There is a modulo operation in the constructor of CashManager.sol, so it's not possible to initially set epochDuration to zero because the transaction would fail.

currentEpochStartTimestamp =
      block.timestamp -
      (block.timestamp % epochDuration);

However, this is not the case for setEpochDuration where it's possible to set the epochDuration to zero. If this happens, any operation that requires advancing the epoch (including token redemption, collateral distribution, and setting the pending redemption balance) will revert.

Impact

epochDuration set to zero prevents all major CashManager features (token redemption, collateral distribution, setting the pending redemption balance).

Proof of Concept

function transitionEpoch() public {
    //@audit epochDuration = 0 leads to division by zero
    uint256 epochDifference = (block.timestamp - currentEpochStartTimestamp) /
      epochDuration; 
    if (epochDifference > 0) {
      currentRedeemAmount = 0;
      currentMintAmount = 0;
      currentEpoch += epochDifference;
      currentEpochStartTimestamp =
        block.timestamp -
        (block.timestamp % epochDuration);
    }
  }

Tools Used

VS Code

Recommended Mitigation Steps

Add a check inside setEpochDuration with a minimum value of 1 second.

function setEpochDuration(
    uint256 _epochDuration
  ) external onlyRole(MANAGER_ADMIN) {
    //@audit add check for at least 1 second
    // if(_epochDuration == 0)
    //    revert ZeroEpochDuration();
    uint256 oldEpochDuration = epochDuration;
    epochDuration = _epochDuration;
    emit EpochDurationSet(oldEpochDuration, _epochDuration);
  }

QA Report

See the markdown file with the details of this report here.

Function getAccRewardPerToken() returns value of _accRewardsPerToken in RAY format, while it should be in WAD format

Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/aave/StaticATokenLM.sol#L599

Vulnerability details

Impact

Variable _unclaimedRewards is presented in RAY format as pointed here.

Function getUnclaimedRewards() when returning _unclaimedRewards translates it into WAD format.

Variable _userSnapshotRewardsPerToken is also in RAY format as stated here.

Other variables should be in WAD format. For example, _lifetimeRewards in WAD format.

However, variable _accRewardsPerToken is in RAY format even it's not declared explicitly. It's possible to infer this fact from the code.

And finally function getAccRewardPerToken() doesn't translate it to WAD.

So entity, calling this function and expecting WAD number will get a way bigger value.

Proof of Concept

Recommended Mitigation Steps

Translate _accRewardsPerToken to WAD inside getAccRewardPerToken().

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

addKYCAddressViaSignature function does not check if the user address is on the sanctions list before granting them KYC status

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/kyc/KYCRegistry.sol#L79

Vulnerability details

Impact

It can lead to sanctions-banned individuals or entities gaining access to the products or services that are restricted to only KYC verified individuals/entities. This can put the company at risk of legal and financial repercussions.
Or may be, It can also compromise the integrity of the KYC verification process and potentially allow malicious actors to bypass the system and gain access to restricted resources.

Proof of Concept

pragma solidity 0.8.16;

import "contracts/cash/interfaces/IKYCRegistry.sol";
import "contracts/cash/external/chainalysis/ISanctionsList.sol";

contract KYCRegistry {
// Chainalysis sanctions list
ISanctionsList public immutable sanctionsList;

// { => { => is user KYC approved}
mapping(uint256 => mapping(address => bool)) public kycState;

/// @notice constructor
constructor(address _sanctionsList) {
sanctionsList = ISanctionsList(_sanctionsList);
}

/**

  • @notice Add a provided user to the registry at a specified
  •     `kycRequirementGroup`. In order to sucessfully call this function,
    
  •     An external caller must provide a signature signed by an address
    
  •     with the role `kycGroupRoles[kycRequirementGroup]`.
    
  • @param kycRequirementGroup KYC requirement group to modify user's
  •                        KYC status for
    
  • @param user User address to change KYC status for

*/
function addKYCAddress(
uint256 kycRequirementGroup,
address user
) external {
require(
!kycState[kycRequirementGroup][user],
"KYCRegistry: user already verified"
);
require(!sanctionsList.isSanctioned(user), "KYCRegistry: user is on sanctions list");
kycState[kycRequirementGroup][user] = true;
}
}

Tools Used


Recommended Mitigation Steps

The mitigation steps should be,
Adding a check that verifies whether the user address is on the sanctions list before granting them KYC status. This can be done by calling the isSanctioned() function on the sanctions list contract and passing in the user address as a parameter.

If the user is on the sanctions list, the smart contract should revert the transaction and not grant the user KYC status.

QA Report

See the markdown file with the details of this report here.

Lack of Access Control for Pauser Role

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/token/Cash.sol #L0

Vulnerability details

Impact

Description: The contract is using the "ERC20PresetMinterPauserUpgradeable" contract from OpenZeppelin which has a pauser role that allows anyone to pause and unpause the contract. However, the contract does not have any mechanism to restrict who can pause and unpause the contract. This can be exploited by an attacker to pause the contract, preventing legitimate users from transferring tokens.

Proof of Concept (Solution)

1	Import the "OpenZeppelin Access Control" library:

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.0/contracts/access/Roles.sol";

2	Add a pauser role to the contract:

contract Cash is ERC20PresetMinterPauserUpgradeable, Roles {
address public pauser;
// ...
}

3	Initialize the pauser role in the constructor

constructor() {
_disableInitializers();
pauser = msg.sender;
addRole(pauser, "pauser");
}

4	Add a require statement to the pause and unpause functions to check that the msg.sender has the pauser role:

function pause() public {
require(hasRole(msg.sender, "pauser"), "Cash: msg.sender must have pauser role to pause the contract.");
super.pause();
}

function unpause() public {
require(hasRole(msg.sender, "pauser"), "Cash: msg.sender must have pauser role to unpause the contract.");
super.unpause();
}

Tools Used

Remix, Ganache, others.

Recommended Mitigation Steps

To mitigate this vulnerability, access control mechanisms should be added to restrict who can pause and unpause the contract. This can be achieved by adding a pauser role and using the "OpenZeppelin Access Control" library to restrict access to the pauser role. With these changes, the contract will only allow the address that deploy the contract to pause and unpause the contract, preventing an attacker from pausing the contract and preventing legitimate users from transferring tokens.

High-Risk Single Point of Failure and Critical Lack of Access Control Vulnerability in CCashDelegate Smart Contract

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L30-L34
https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L45-L50
https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L21
https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L39

Vulnerability details

Impact

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L30-L34

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L45-L50

      msg.sender == admin,
      "only the admin may call _becomeImplementation"
    );

&

require(
      msg.sender == admin,
      "only the admin may call _resignImplementation"
    );

These lines of code in the _becomeImplementation and _resignImplementation functions are checking that the msg.sender (the address of the account that called the function) is equal to the admin variable. If it is not, then the require statement will cause the function to revert and the call will fail. This means that only the address that is stored in the admin variable can call these functions, which could make the contract unable to function if the admin's address is compromised or the admin is unable to perform their duties.

If the admin's address is compromised, an attacker could take control of the contract and use it to steal assets, manipulate data, or launch further attacks.

If the admin is unable to perform their duties, the contract will be unable to function, which could result in a loss of assets or the inability to perform critical functions.

In case of a crisis, the contract would be unable to be stopped, which could create an emergency where the contract's assets could be at risk.

This vulnerability could also impact the contract's scalability and usability, as there is no contingency plan in case the admin's address is compromised or the admin is unable to perform their duties.

Proof of Concept

The attacker can obtain the address of the admin: This can be done by calling the admin() function on the contract or by inspecting the contract's code.

An attacker can compromise the admin's address. This can be done by various means such as phishing, social engineering, or other hacking methods that could lead to the attacker gaining control over the admin's address.

Call the _becomeImplementation or _resignImplementation functions: Once the attacker has the admin's address, they can call the _becomeImplementation or _resignImplementation functions, which should only be able to be called by the admin.

The attacker could also take control of the contract by calling the _becomeImplementation function and potentially steal assets or manipulate data.

Here is an example.

// Step 1: Obtain the admin address
address admin = CCashDelegate.admin();

// Step 2: Compromise the admin's address (not shown)

// Step 3: Call the _resignImplementation function
CCashDelegate.resignImplementation();

// Step 4: Observe the impact
// The contract will be unable to function as the _resignImplementation function can only be called by the admin

Tools Used

Manual audit, Vs Code.

Recommended Mitigation Steps

The first step is to Implement a multi-signature mechanism. Allow multiple addresses to have administrative capabilities on the contract. This can be done by implementing a multi-signature mechanism that requires a certain number of signatures to perform certain actions.

Implement an emergency stop mechanism. Allow the contract to be halted in case of a critical security vulnerability or other emergencies. This can be done by implementing a mechanism that allows a specific address or group of addresses to halt the contract's functionality.

Implement a mechanism to change the admin address. Allow the admin address to be changed in case the current one is compromised or unable to perform its duties.

Regularly review and update the contract to ensure that it is secure and functional.

Code snippet for a multi-signature mechanism.

// Declare an array of addresses for the admin
address[] adminAddresses;

// Update the admin function to add the new admin
function addAdmin(address _newAdmin) public {
    require(msg.sender == adminAddresses[0], "Only the current admin can add a new admin.");
    adminAddresses.push(_newAdmin);
}

// Update the _becomeImplementation function to require multiple signatures
function _becomeImplementation(bytes memory data) public virtual override {
    // Shh -- currently unused
    data;

    // Shh -- we don't ever want this hook to be marked pure
    if (false) {
      implementation = address(0);
    }

    require(
      address(this).hasAuthority(msg.sender, adminAddresses),
      "only adminAddresses may call _becomeImplementation"
    );
}

Impact

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L21

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/tokens/cCash/CCashDelegate.sol#L39

function _becomeImplementation(bytes memory data) public virtual override {

function _resignImplementation() public virtual override {

This is caused by the fact that the _becomeImplementation and _resignImplementation functions are defined as "public" which means that anyone can call these functions if they have the contract's address, regardless of whether or not they have the necessary permissions to do so. There is no mechanism in place to restrict the functions to specific addresses or roles, which could allow unauthorized users to access sensitive data or manipulate the contract's state.

An attacker could call the _becomeImplementation or _resignImplementation functions and take control of the contract, stealing assets or manipulating data.

An attacker could access sensitive information stored in the contract, such as private keys or user data, by calling functions that are intended to be private.

An unscrupulous attacker could create a malicious contract that calls the functions in the contract with unintended parameters, causing unexpected behavior.

A hacker could cause a denial-of-service attack by repeatedly calling the functions, causing the contract to run out of gas or become unresponsive.
It could also impact the contract's scalability and usability, as any address can call the functions without any restriction.

Proof of Concept

  1. Obtain the address of the contract This can be done by deploying the contract to a testnet or by inspecting the contract's code.

  2. Call the _becomeImplementation or _resignImplementation functions: With the contract address, anyone can call the _becomeImplementation or _resignImplementation functions.

  3. Observe the impact of the vulnerability as a result of the attacker's actions, the contract should be manipulated by the attacker, and sensitive data could be exposed or stolen.

The following is an example of an attacker who could use to exploit the vulnerability.

// Step 1: Obtain the contract address
address contractAddress = "0x1234....";

// Step 2: Create a contract instance
CCashDelegate contract = CCashDelegate(contractAddress);

// Step 3: Call the _resignImplementation function
contract.resignImplementation();

// Step 4: Observe the impact
// The contract can be manipulated by the attacker, and sensitive data could be exposed or stolen

Tools Used

Manual audit, vs code

Recommended Mitigation Steps

To mitigate this Vulnerability below is a recommended step.

  1. Implement role-based access control to restrict the functions to specific addresses or roles by using an access control library such as OpenZeppelin's AccessControl This will ensure that only authorized users can call the functions.

  2. Use the internal visibility keyword to change the visibility of the functions to "internal" so that only contracts that inherit from the contract can call the functions.

  3. Use the "private" visibility keyword to change the visibility of the functions to "private" so that only the contract's code can call the functions.

  4. Implement a circuit breaker mechanism Implement a mechanism that allows a specific address or group of addresses to halt the contract's functionality in case of an emergency.

import "https://github.com/OpenZeppelin/openzeppelin-contracts/contracts/access/Roles.sol";

contract CCashDelegate is CCash, CDelegateInterface, Roles {
  // Declare a role for the admin
  using Roles for Roles.Role;
  Roles.Role admin;

  // Update the _becomeImplementation function to require the admin role
  function _becomeImplementation(bytes memory data) public virtual override {
    // Shh -- currently unused
    data;

    // Shh -- we don't ever want this hook to be marked pure
    if (false) {
      implementation = address(0);
    }

    require(
      admin.has(msg.sender),
      "only the admin may call _becomeImplementation"
    );
  }
}

QA Report

See the markdown file with the details of this report here.

Timestamp dependency, block.timestamp is vulnerable to manipulation

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L175
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L176
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L577
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L584
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L585
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/kyc/KYCRegistry.sol#L92
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/OndoPriceOracleV2.sol#L294

Vulnerability details

Impact

The vulnerability of block.timestamp in smart contracts is related to the fact that the timestamp of a block is provided by the miner who mined the block. As a result, the timestamp is not guaranteed to be accurate or to be the same across different nodes in the network. In particular, an attacker can potentially mine a block with a timestamp that is favorable to them, known as "selective packing".

For example, an attacker could mine a block with a timestamp that is slightly in the future, allowing them to bypass a time-based restriction in a smart contract that relies on block.timestamp. This could potentially allow the attacker to execute a malicious action that would otherwise be blocked by the restriction.

Proof of Concept

contracts/cash/CashManager.sol

contracts/cash/kyc/KYCRegistry.sol

contracts/lending/OndoPriceOracleV2.sol

Tools Used

  • Private self-made tool for static analysis
  • Manual Review, Remix IDE

Recommended Mitigation Steps

Developers should avoid using block.timestamp in their smart contracts and instead use an alternative timestamp source, such as an oracle, that is not susceptible to manipulation by a miner.

References:

Reentrancy attack vulnerability

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cCash/CCashDelegate.sol #L32,#L43,#L38,#L49

Vulnerability details

Impact

โ€ข No protection against reentrancy attacks: There is no protection against reentrancy attacks in the contract, allowing an attacker to repeatedly call a function and potentially manipulate the contract's state.
โ€ข Lack of proper access control: The contract only checks that the msg.sender is the admin in the _becomeImplementation and _resignImplementation functions, but does not check if the admin is still the same address that was set when the contract was deployed. This allows an attacker who gains control of the admin address to perform malicious actions on the contract.

Proof of Concept

Here is an example of how it can be implemented in the _becomeImplementation function:
bool mutex;

function _becomeImplementation(bytes memory data) public virtual override {
require(!mutex);
mutex = true;

// original code

mutex = false;

}

Here is an example of how it can be implemented in the _becomeImplementation function:
address owner;
address admin;

constructor() public {
owner = msg.sender;
admin = msg.sender;
}

function _becomeImplementation(bytes memory data) public virtual override {
require(msg.sender == owner || msg.sender == admin, "Caller is not authorized");

// original code

}

Tools Used

Remix, Ganache, others..

Recommended Mitigation Steps

A well-known pattern to prevent reentrancy attacks is the use of a mutex, which can be implemented using a mutex variable that is checked and set before and after the critical section of code.
The contract should check that the msg.sender is the address that deployed the contract, or a predefined set of addresses with the appropriate permissions.

[DoS] Requesting a redemption for 1 token causes completeRedemptions to revert.

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/CashManager.sol#L60-L61
https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/CashManager.sol#L724
https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/CashManager.sol#L758-L760

Vulnerability details

Impact

A malicious user is able to cause denial of service by submitting redemptionRequests for 1 token. This is possible due to the fact that at contract initialisation minimumRedeemAmount is initialised at 0. This allow us to always pass the if (amountCashToRedeem < minimumRedeemAmount) { revert WithdrawRequestAmountTooSmall(); } check, as long as value != 0 (this is checked in _checkAndUpdateRedeemLimit).

As specified in the documentation:

For the initial launch, the CASH protocol's CashManager contract will receive USDC and mint a Cash token, whose name will be "OUSG".

We are dealing with a token decimal issue (6 decimals for USDC, 18 decimals for the ERC20), this causes issues in the calculation.

When the redemption requests is successful and included in the next batch of completeRedemptions, the amount for the malicious user when sending a redemptionRequest of 1 gets rounded to 0, causing the _processRedemption function to revert. This means that none of the redemptions go through.

Proof of Concept

Add the following test to Redemption.t.sol:

function test_redeem_single_to_cause_dos() public {
        _seed(100e18, 100e18, 100e18);
        // Have alice request to withdraw 100 cash tokens
        vm.startPrank(alice);
        tokenProxied.approve(address(cashManager), 100e18);
        cashManager.requestRedemption(100e18);
        vm.stopPrank();

        // Charlie withdraws a single token
        vm.startPrank(bob);
        tokenProxied.approve(address(cashManager), 100e18);
        cashManager.requestRedemption(1);
        vm.stopPrank();

        // Move forward to the next epoch
        vm.warp(block.timestamp + 1 days);
        vm.prank(managerAdmin);
        cashManager.setMintExchangeRate(125e3, 0);

        // Approve the cashMinter contract from the assetSender account
        _seedSenderWithCollateral(125e6);

        toWithdraw.push(alice);
        toWithdraw.push(charlie);

        vm.prank(managerAdmin);
        // Expect a revert due to the redemption being rounded to 0
        vm.expectRevert(ICashManager.CollateralRedemptionTooSmall.selector);
        cashManager.completeRedemptions(toWithdraw, toRefund, 125e6, 0, 5e6);
    }

Tools Used

Manual review

Recommended Mitigation Steps

Initialise minimumRedeemAmount to a larger value, or account better for the decimals.

User can be added back to KYCed list after being removed using removeKYCAddresses

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/kyc/KYCRegistry.sol#L79-L112
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/kyc/KYCRegistry.sol#L175

Vulnerability details

Impact

A user that should be removed from the KYC list can be added back to the KYCed list using addKYCAddressViaSignature() until the signature expires.

Proof of Concept

  • User submits KYC documents and gets a valid signature
  • User submits the signature to get whitelisted using addKYCAddressViaSignature()
  • Team revokes KYC using removeKYCAddresses
  • User resubmits signature using addKYCAddressViaSignature(), the address will be added back to the whitelist

Tools Used

Manual review

Recommended Mitigation Steps

I recommend adding a storing and checking for used signatures to prevent them from being reused. It's also lacking a mechanism to revoke not used signatures.

QA Report

See the markdown file with the details of this report here.

fToken's underlying asset price check

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/lending/OndoPriceOracleV2.sol#L92-L119

Vulnerability details

Impact

The getUnderlyingPrice function is missing a check to see if the fToken's underlying asset price is capped correctly

Proof of Concept

https://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/ link to an historical exploit where a similar error helped the attacker drain funds from the contract

The DAO smart contract had a significant vulnerability due to the absence of a price cap check on the DAO tokens, which enabled an attacker to acquire a large number of tokens at a low cost and then exploit another vulnerability in the smart contract to transfer a significant amount of the fund's assets to a child DAO under the attacker's control. As a result of this vulnerability, the attacker was able to exploit the low price of the tokens to siphon off a large sum of the fund's assets.

Tools Used

solidity knowledge, ethereum exploit knowledge, remix

Recommended Mitigation Steps

this is how I imagined modifying the getUnderlyingPrice function to have this extra security step

image

This new implementation uses the fTokenToOracleAddress mapping to get the address of the Chainlink oracle contract associated with the fToken. Then it creates an instance of the AggregatorV3Interface contract using that address and calls the latestAnswer() function to get the latest value of the price. The returned value is a tuple, where the first element is the timestamp of the latest value and the second element is the value itself. In the code, it checks if timestamp returned is greater than zero, meaning that the oracle returned a valid value, if not, it throws an error message.

this code assumes that the fTokenToOracleAddress, AggregatorV3Interface and the latestAnswer() function have been properly implemented and initialized in the smart contract

SWC-131 Presence of unused variables CCashDelegate

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cCash/CCashDelegate.sol#L21

Vulnerability details

Proof of Concept

the contract https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cCash/CCashDelegate.sol#L21 have a function _becomeImplementation which imports as argument a data parameter but it never used :

function _becomeImplementation(bytes memory data) public virtual override {
    // Shh -- currently unused
    data;

    // Shh -- we don't ever want this hook to be marked pure
    if (false) {
      implementation = address(0);
    }

    require(
      msg.sender == admin,
      "only the admin may call _becomeImplementation"
    );
  }

As taken from "https://swcregistry.io/docs/SWC-131" :

Unused variables are allowed in Solidity and they do not pose a direct security issue. It is best practice though to avoid them as they can:

cause an increase in computations (and unnecessary gas consumption)
indicate bugs or malformed data structures and they are generally a sign of poor code quality
cause code noise and decrease readability of the code

Tools Used

Manual review

Recommended Mitigation Steps

Remove unused parameters

QA Report

See the markdown file with the details of this report here.

CashManager's minting & redemption limits are dangerously low

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L212
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L676
https://github.com/code-423n4/2023-01-ondo/blob/main/deploy/cash/local/Cash/deploy_cashManager.ts#L43-L45

Vulnerability details

Impact

The CashManager limits the number of tokens that can be minted & redeemed per epoch. According to the deployment script, the mintLimit is 10,000e6 and the redeemLimit is 10,000e18. The deposited token is USDC so per epoch (1 day) you only need to deposit 10,000$ to reach the limit. That is dangerously low so that someone who deposits and redeems regularly is able to lock out other users.

If we assume that mints and redemptions are handled directly after an epoch ended by the admin team, a malicious attacker can DOS the protocol by requesting a deposit at the start of an epoch, $n$, and requesting a withdrawal at the start of the very next epoch. By depositing and withdrawing in the same epoch (effectively you lock up 20,000$) they can prevent other users from requesting deposits or withdrawals. We can assume that there should be no significant costs to the user other than the gas they have to pay as well as the opportunity costs of 20,000$.

There are some ways the admin team can stop the attacker:

  • don't process their deposit / withdrawal
  • revoke their access
  • cancel their deposit / withdrawal

I'm not sure how viable those solutions are considering that the user is doing nothing "illegal". Even then, the user was able to DOS the protocol temporarily.

Proof of Concept

When a user requests a deposit or withdrawal the protocol verifies that the limit is not exceeded:

  function _checkAndUpdateRedeemLimit(uint256 amount) private {
    if (amount == 0) {
      revert RedeemAmountCannotBeZero();
    }
    if (amount > redeemLimit - currentRedeemAmount) {
      revert RedeemExceedsRateLimit();
    }

    currentRedeemAmount += amount;
  }

  function _checkAndUpdateMintLimit(uint256 collateralAmountIn) private {
    if (collateralAmountIn > mintLimit - currentMintAmount) {
      revert MintExceedsRateLimit();
    }

    currentMintAmount += collateralAmountIn;
  }

In the deployment script, the limits are set to 10,000:

  await deploy("CashManager", {
    from: deployer,

    args: [
      USDC_ADDRESS,
      cashProxiedContract.address,
      guardian.address,
      pauser.address,
      assetRecipient.address,
      assetSender.address,
      feeRecipient.address,
      parseUnits("10000", 6), // mint limit
      parseUnits("10000", 18), // redeem limit
      BigNumber.from(86400), // epoch duration seconds
      registry.address, // KYC registry
      KYC_GROUP,
    ],
    log: true,
  });

Tools Used

none

Recommended Mitigation Steps

You either remove the limits or set them so high that the economic costs are high enough to make the attack unviable.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Unused state variables

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cCash/CCashDelegate.sol #L28

Vulnerability details

Impact

The 'data' variable in the _becomeImplementation function is unused and can be safely removed from the contract.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Remix, Ganache, others.

Recommended Mitigation Steps

Remove the 'data' variable from the _becomeImplementation function. This will not only make the code more readable, but also save gas costs.

using the low-level ".call()" & "delegatecall()" function but there is no check for contract existence.

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/external/openzeppelin/contracts-upgradeable/token/ERC721/ERC721BurnableUpgradeable.sol#L30
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/compound/governance/GovernanceBravoDelegator.sol#L68

Vulnerability details

Impact

The actual impact of not calling the built-in "_burn()" function of the ERC-721 standard in the "burn" function is that the token may not be properly removed from circulation and the total supply may not be updated correctly. This could cause problems with tracking the total supply of tokens and potentially lead to inflation.

The function does not call the ERC721 _burn() function so it may not correctly perform the necessary steps to remove a token from circulation, such as updating the balance of the token owner or updating the total supply.

Additionally, since the function is only checking if the caller is the token owner or has been approved by the token owner, it may not be enough to ensure the correct authorization to burn the token, and it could lead to vulnerabilities if the function is not properly implemented or if there are bugs in the custom function "_isApprovedOrOwner(_msgSender(), tokenId)".

Overall, not calling the built-in "_burn()" function and not properly checking the caller's authorization to burn the token could both lead to potential security issues and should be carefully considered and tested in the smart contract.

Proof of Concept

At https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/external/openzeppelin/contracts-upgradeable/token/ERC721/ERC721BurnableUpgradeable.sol#L30

  function burn(uint256 tokenId) public virtual {
    //solhint-disable-next-line max-line-length
    require(
      _isApprovedOrOwner(_msgSender(), tokenId),
      "ERC721: caller is not token owner nor approved"
    );
    _burn(tokenId);
  }

Tools Used

Manual VS Code review

Recommended Mitigation Steps

Include the following line in the burn() function _burn();

`MANAGER_ADMIN`'s change to epoch exchange rate will not be reflected on cash tokens already redeemed

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L366-L385

Vulnerability details

Impact

If the MANAGER_ADMIN overrides the epoch exchange rate by calling overrideExchangeRate(), users who have already claimed CASH through claimMint() will not have this change in exchange rate reflected in their CASH balance.

Proof of Concept

  1. User calls requestMint() during epoch x with collateral amount 1.
  2. After epoch x concludes, SETTER_ADMIN calls setMintExchangeRate() to set the exchange rate of epoch x to 1.
  3. User calls claimMint() and receives CASH amount based on collateral amount = 1 and exchange rate = 1 for epoch x.
  4. MANAGER_ADMIN calls overrideExchangeRate() to override epoch exchange rate to the correct value of 10. The user now only has a tenth of the CASH tokens they are supposed to have.

Tools used

Manual code inspection

Recommended Mitigation Steps

Store a list of users who claimed cash tokens for a given epoch. When the overrideExchangeRate() function is called by the MANAGER_ADMIN, calculate the cash difference owed to / owing by the user, and adjust the user's cash token balance accordingly.

QA Report

See the markdown file with the details of this report here.

No Transfer Ownership Pattern

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/83a45fa7cbc427a62ccf04d0e5cf9b0e780ec26c/contracts/cash/factory/CashFactory.sol#L96
https://github.com/code-423n4/2023-01-ondo/blob/83a45fa7cbc427a62ccf04d0e5cf9b0e780ec26c/contracts/cash/factory/CashKYCSenderReceiverFactory.sol#L105
https://github.com/code-423n4/2023-01-ondo/blob/83a45fa7cbc427a62ccf04d0e5cf9b0e780ec26c/contracts/cash/factory/CashKYCSenderFactory.sol#L105

Vulnerability details

Impact

The current ownership transfer process involves the current owner calling transferOwnership(). This function checks the new owner is not the zero address and proceeds to write the new owner's address into the owner's state variable. If the nominated EOA account is not a valid account, it is entirely possible the owner may accidentally transfer ownership to an uncontrolled account, breaking all functions with the onlyOwner() modifier.

This could impact availability of protocol

Proof of Concept

https://github.com/code-423n4/2023-01-ondo/blob/83a45fa7cbc427a62ccf04d0e5cf9b0e780ec26c/contracts/cash/factory/CashKYCSenderFactory.sol#L105

https://github.com/code-423n4/2023-01-ondo/blob/83a45fa7cbc427a62ccf04d0e5cf9b0e780ec26c/contracts/cash/factory/CashKYCSenderReceiverFactory.sol#L105

https://github.com/code-423n4/2023-01-ondo/blob/83a45fa7cbc427a62ccf04d0e5cf9b0e780ec26c/contracts/cash/factory/CashFactory.sol#L96

The transferOwnership implementation in Openzeppelin is like this below

  /**
   * @dev Transfers ownership of the contract to a new account (`newOwner`).
   * Can only be called by the current owner.
   */
  function transferOwnership(address newOwner) public virtual onlyOwner {
    require(newOwner != address(0), "Ownable: new owner is the zero address");
    _transferOwnership(newOwner);
  }

  /**
   * @dev Transfers ownership of the contract to a new account (`newOwner`).
   * Internal function without access restriction.
   */
  function _transferOwnership(address newOwner) internal virtual {
    address oldOwner = _owner;
    _owner = newOwner;
    emit OwnershipTransferred(oldOwner, newOwner);
  }

Tools Used

Manual reading

Recommended Mitigation Steps

Consider implementing a two step process where the owner nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.

QA Report

See the markdown file with the details of this report here.

Lack of Access Control for Minter Role

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/token/Cash.sol #L0

Vulnerability details

Impact

The contract is using the "ERC20PresetMinterPauserUpgradeable" contract from OpenZeppelin which has a minter role that allows anyone to mint new tokens. However, the contract does not have any mechanism to restrict who can mint new tokens. This can be exploited by an attacker to mint an unlimited number of tokens, leading to inflation of the token supply.

Proof of Concept (Solution)

1	Import the "OpenZeppelin Access Control" library:

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.0/contracts/access/Roles.sol";

2	Add a minter role to the contract:

contract Cash is ERC20PresetMinterPauserUpgradeable, Roles {
address public minter;
// ...
}

3	Initialize the minter role in the constructor:

constructor() {
_disableInitializers();
minter = msg.sender;
addRole(minter, "minter");
}

4	Add a require statement to the mint function to check that the msg.sender has the minter role:

function mint(address to, uint256 amount) public {
require(hasRole(msg.sender, "minter"), "Cash: msg.sender must have minter role to mint tokens.");
_mint(to, amount);
}

Tools Used

Remix, Ganache, others.

Recommended Mitigation Steps

To mitigate this vulnerability, access control mechanisms should be added to restrict who can mint new tokens. This can be achieved by adding a minter role and using the "OpenZeppelin Access Control" library to restrict access to the minter role. With these changes, the contract will only allow the address that deploy the contract to mint new tokens, preventing an attacker from minting an unlimited number of tokens.

Use safe ERC721 mint

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L791
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/external/openzeppelin/contracts-upgradeable/token/ERC721/ERC721PresetMinterPauserAutoIdUpgradeable.sol#L102
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/external/openzeppelin/contracts-upgradeable/token/ERC721/ERC721PresetMinterPauserAutoIdUpgradeable.sol#L110
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cErc20ModifiedDelegator.sol#L579
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cErc20ModifiedDelegator.sol#L755
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cCash/CCash.sol#L66
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cCash/CTokenInterfacesModifiedCash.sol#L393
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cToken/CTokenInterfacesModified.sol#L391
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/tokens/cToken/CErc20.sol#L66

Vulnerability details

Impact

the mint() function in this function does not cause any direct impact on the security of this function, as long as the mint() function in the cash contract has been implemented securely and has been properly audited. However, minting new tokens can be a security-sensitive operation, and it's always important to check the implementation of the mint() function in the cash contract to make sure that it is implemented in a secure manner and has been properly audited.

Proof of Concept

At https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L791

  function _processRefund(
    address[] calldata refundees,
    uint256 epochToService
  ) private returns (uint256 totalCashAmountRefunded) {
    uint256 size = refundees.length;
    for (uint256 i = 0; i < size; ++i) {
      address refundee = refundees[i];
      uint256 cashAmountBurned = redemptionInfoPerEpoch[epochToService]
        .addressToBurnAmt[refundee];
      redemptionInfoPerEpoch[epochToService].addressToBurnAmt[refundee] = 0;
      cash.mint(refundee, cashAmountBurned);
      totalCashAmountRefunded += cashAmountBurned;
      emit RefundIssued(refundee, cashAmountBurned, epochToService);
    }
    return totalCashAmountRefunded;
  }

Tools Used

Manual VS Code review

Recommended Mitigation Steps

My recommendation from my previous experience is to use safeMint

cash.safeMint(refundee, cashAmountBurned);

QA Report

See the markdown file with the details of this report here.

oracle price from Chainlink might be stale due to all assets price feeds sharing the same maxChainlinkOracleTimeDelay

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/OndoPriceOracleV2.sol#L77
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/lending/OndoPriceOracleV2.sol#L277-L301

Vulnerability details

Impact

maxChainlinkOracleTimeDelay is shared across all assets price feeds from Chainlink. This will cause some assets price feeds to have stale prices if the maxChainlinkOracleTimeDelay is set to a high value, and cause some assets price feeds to revert frequently if maxChainlinkOracleTimeDelay is set to a low value.

Proof of Concept

maxChainlinkOracleTimeDelay determines whether the oracle price returned from Chainlink is stale, synonomous to what other protocol calls a heartbeat.

  uint256 public maxChainlinkOracleTimeDelay = 90000; // 25 hours

The initial maxChainlinkOracleTimeDelay is 90000, 25 hours. This is not an issue as setMaxChainlinkOracleTimeDelay can override this value. The issue arises from the fact that all assets price feeds from Chainlink use the same maxChainlinkOracleTimeDeplay. Setting it to 3 hours might result in stale prices as some price feeds have heartbeats of 20 mins. Setting it to 20 mins will cause other price feeds with slower heartbeats to revert frequently due to the small tolerance that their slower heartbeats cannot meet.

  function getChainlinkOraclePrice(
    address fToken
  ) public view returns (uint256) {
    require(
      fTokenToOracleType[fToken] == OracleType.CHAINLINK,
      "fToken is not configured for Chainlink oracle"
    );
    ChainlinkOracleInfo memory chainlinkInfo = fTokenToChainlinkOracle[fToken];
    (
      uint80 roundId,
      int answer,
      ,
      uint updatedAt,
      uint80 answeredInRound
    ) = chainlinkInfo.oracle.latestRoundData();
    require(
      (answeredInRound >= roundId) &&
        (updatedAt >= block.timestamp - maxChainlinkOracleTimeDelay),
      "Chainlink oracle price is stale"
    );
    require(answer >= 0, "Price cannot be negative");
    // Scale to decimals needed in Comptroller (18 decimal underlying -> 18 decimals; 6 decimal underlying -> 30 decimals)
    // Scales by same conversion factor as in Compound Oracle
    return uint256(answer) * chainlinkInfo.scaleFactor;
  }

Tools Used

Manual Review

Recommended Mitigation Steps

Recommend setting different maxChainlinkOracleTimeDelay for different price feeds.

Allowing anybody to claim mints can cause a loss of funds if the exchange rate is updated for an epoch

Lines of code

https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L241
https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/CashManager.sol#L366

Vulnerability details

Impact

The admin has the ability to override the exchange rate of an epoch. So there's the possibility of there being multiple rates at which you can claim your mint. A mint for a given user can be claimed by anybody. That opens up the possibility of a grieving attack if the new exchange rate is higher than the previous one by claiming the mints before the new exchange rate is set. That causes a loss of funds for the users.

Proof of Concept

claimMint() can be called by anyone:

  function claimMint(
    address user,
    uint256 epochToClaim
  ) external override updateEpoch nonReentrant whenNotPaused checkKYC(user) {
    uint256 collateralDeposited = mintRequestsPerEpoch[epochToClaim][user];
    if (collateralDeposited == 0) {
      revert NoCashToClaim();
    }
    if (epochToExchangeRate[epochToClaim] == 0) {
      revert ExchangeRateNotSet();
    }

    // Get the amount of CASH due at a given rate per epoch
    uint256 cashOwed = _getMintAmountForEpoch(
      collateralDeposited,
      epochToClaim
    );

    mintRequestsPerEpoch[epochToClaim][user] = 0;
    cash.mint(user, cashOwed);

    emit MintCompleted(
      user,
      cashOwed,
      collateralDeposited,
      epochToExchangeRate[epochToClaim],
      epochToClaim
    );
  }

The admin is able to override an existing exchange rate for an epoch through overrideExchangeRate():

  function overrideExchangeRate(
    uint256 correctExchangeRate,
    uint256 epochToSet,
    uint256 _lastSetMintExchangeRate
  ) external override updateEpoch onlyRole(MANAGER_ADMIN) {
    if (epochToSet >= currentEpoch) {
      revert MustServicePastEpoch();
    }
    uint256 incorrectRate = epochToExchangeRate[epochToSet];
    epochToExchangeRate[epochToSet] = correctExchangeRate;
    if (_lastSetMintExchangeRate != 0) {
      lastSetMintExchangeRate = _lastSetMintExchangeRate;
    }
    emit MintExchangeRateOverridden(
      epochToSet,
      incorrectRate,
      correctExchangeRate,
      lastSetMintExchangeRate
    );
  }

The attacker has to watch the mempool for a tx that overrides the exchange rate. If it is set higher than the previous one, they frontrun the tx where they claim every eligible user's mint.

Tools Used

none

Recommended Mitigation Steps

The easiest solution is to only allow the user themselves to claim their mint.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.