GithubHelp home page GithubHelp logo

2021-05-visorfinance-findings's People

Contributors

c4-staff avatar code423n4 avatar ninek9 avatar

Watchers

 avatar  avatar

2021-05-visorfinance-findings's Issues

Unhandled return value of transferFrom in timeLockERC20() could lead to fund loss for recipients

Handle

0xRajeev

Vulnerability details

Impact

ERC20 implementations are not always consistent. Some implementations of transfer and transferFrom could return ‘false’ on failure instead of reverting. It is safer to wrap such calls into require() statements or use safe wrapper functions implementing return value/data checks to handle these failures. For reference, see similar Medium-severity finding from Consensys Diligence Audit of Aave Protocol V2: https://consensys.net/diligence/audits/2020/09/aave-protocol-v2/#unhandled-return-values-of-transfer-and-transferfrom

While the contract uses Uniswap’s TransferHelper library functions safeTransfer in other places for ERC20 tokens or OpenZeppelin’s saferTransferFrom for ERC721 tokens (both of which call the token’s transfer/transferFrom functions and check return value for success and return data), it misses using TransferHelper.safeTransferFrom in this one case on L610 in timeLockERC20() when tokens are transferred from owner to the vault and instead directly uses the token’s transferFrom() call without checking for its return value.

The impact can be that for an arbitrary ERC20 token, this transferFrom() call may return failure but the vault logic misses that, assumes it was successfully transferred into the vault and updates the timelockERC20Balances accounting accordingly. The timeUnlockERC20(), transferERC20() or delegatedTransferERC20() calls for that token will fail because the vault contract balance would have less tokens than accounted for in timelockERC20Balances because of the previously failed (but ignored) transferFrom() call.

Proof of Concept

  1. Let’s say Alice owes Bob 100 USD after a week, for which they agree that Alice will pay in 100 tokens of USD stablecoin tokenA.
  2. Alice, the vault owner, calls timeLockERC20() for recipient=Bob, token=tokenA, amount=100 and expiry=1-week-from-then (corresponding Unix timestamp) but tokenA’s implementation does not revert on failure but instead returns true/false. If the transferFrom failed, say because Alice did not have those 100 tokenAs, the return value is ignored on L610 in timeLockERC20() and vault logic considers that it indeed has 100 tokenAs locked for Bob.
  3. Bob looks at the TimeLockERC20 event emitted in the successful timeLockERC20() transaction from Alice and assumes 100 tokenAs are indeed locked by Alice in the vault for him which can be withdrawn after expiry.
  4. After timelock expiry, Bob tries to transfer the 100 tokenAs Alice locked in the vault for him. The TransferHelper.safeTransfer() call on L637 in timeUnlockERC20() fails because the vault has 0 tokenAs because they were never successfully transferred in Step 2.
  5. Bob could thus be tricked into thinking that 100 tokenAs are locked in the vault for him by Alice but they never were. This leads to loss of funds for Bob.

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L610

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L637

Tools Used

Manual Analysis

Recommended Mitigation Steps

Replace use of IERC20(token).transferFrom(msg.sender, address(this), amount); on L610 with:

TransferHelper.safeTransferFrom(token, msg.sender, address(this), amount); as shown in

https://github.com/Uniswap/uniswap-lib/blob/c01640b0f0f1d8a85cba8de378cc48469fcfd9a6/contracts/libraries/TransferHelper.sol#L33-L45

This will revert on transfer failure for e.g. if msg.sender does not have a token balance >= amount.

anyone can call onERC721Received

Handle

paulius.eth

Vulnerability details

Impact

function onERC721Received does not authorize a caller thus anyone can add a new NFT passing arbitrary values to nftContract and tokenId even if they do not send the actual NFT.

Recommended Mitigation Steps

Authorize the caller depending on the intentions and check that the token is actually received.

Use a temporary variable to cache repetitive complex calculation

Handle

0xRajeev

Vulnerability details

Impact

In function delegatedTransferERC20(), the complex calculation keccak256(abi.encodePacked(msg.sender, token)) is performed three times in three different places in the function. This consumes a lot of unnecessary gas which can be saved by saving the calculation in a temporary bytes32 variable and using that instead.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L450

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L459

Tools Used

Manual Analysis

Recommended Mitigation Steps

Save keccak256(abi.encodePacked(msg.sender, token)) in a temporary bytes32 variable and use that in all places.

Change function visibility from public to external

Handle

0xRajeev

Vulnerability details

Impact

Functions getTimeLockCount(), getTimeLockERC721Count(), timeLockERC721(), timeUnlockERC721(), timeLockERC20() and timeUnlockERC20() are never called from within contracts but yet declared public. Their visibility can be made external to save gas.

As described in https://mudit.blog/solidity-gas-optimization-tips/: “For all the public functions, the input parameters are copied to memory automatically, and it costs gas. If your function is only called externally, then you should explicitly mark it as external. External function’s parameters are not copied into memory but are read from calldata directly. This small optimization in your solidity code can save you a lot of gas when the function input parameters are huge.”

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L248

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L253

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L529

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L561

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L583

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L619

Tools Used

Manual Analysis

Recommended Mitigation Steps

Change function visibility from public to external

Misnamed getNftById() function may lead to interface issues

Handle

0xRajeev

Vulnerability details

Impact

getNftById() function appears to implement an interface where you get the NFT by providing the NFT token ID but this function is really implementing getNFTByIndex() because what is passed as parameter is the index into the nfts array and not the tokenID which is actually part of the return value.

If an interface with other tools or protocols is created based on the function name then it will not work because the tokenID provided will not match with the NFT array index.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L225-L232

Tools Used

Manual Analysis

Recommended Mitigation Steps

Change function name to getNFTByIndex().

sandwich approveTransferERC20

Handle

paulius.eth

Vulnerability details

Impact

function approveTransferERC20 is vulnerable to the sandwich attack. Similar to the erc20 approve issue described here: https://blog.smartdec.net/erc20-approve-issue-in-simple-words-a41aaf47bca6
A malicious delegate can scout for a approveTransferERC20 change and sandwich that (delegatedTransferERC20 amount A, approveTransferERC20 amount A->B, delegatedTransferERC20 amount B). It is more of a theoreticall issue and mostly depends on the honesty of the delegators. If we can assume that delegators are trustable actors, then this is very unlikely to happen.

Recommended Mitigation Steps

Possible mitigation could be to replace approveTransferERC20 with increasing/decreasing functions.

Wrong TimeLockERC20 event emitted

Handle

cmichel

Vulnerability details

Vulnerability Details

The Visor.timeLockERC721 function emits the TimeLockERC20 event but should emit TimeLockERC721 instead.

Impact

It allows tricking the backend into registering ERC20 token transfers that never happened which could lead to serious issues when something like an accounting app uses this data.

Recommended Mitigation Steps

Emit the correct event.

A previously timelocked NFT token becomes permanently stuck in vault if it’s ever moved back into the vault

Handle

0xRajeev

Vulnerability details

Impact

Let’s consider a scenario where a particular NFT token was timelocked for a certain duration by the owner using timeLockERC721() with a delegate as the recipient and then transferred out of the vault by the delegate via transferERC721() but without unlocking it explicitly using timeUnlockERC721().

This is possible because transferERC721() does all the timelock checks on expires/block.timestamp and recipient/msg.sender as is done in timeUnlockERC721(). But it misses deleting timelockERC721s[key] for that NFT tokenID (as done in L572 of timeUnlockERC721()).

Because of this missing deletion, if that same NFT is ever put back into the vault later but this time without a timelock, the vault logic still thinks it is a timelocked NFT with the older/stale recipient from earlier because of the missing deletion. So now the owner who makes the transferERC721() call will not match the older/stale recipient address and will fail the check on L510 (unless they control that stale recipient address from the earlier timelock).

The impact is that, without access/control to the earlier timelock recipient, this NFT token is now locked in the vault forever.

Proof of Concept

  1. Alice time locks a particular NFT token with delegate Eve as recipient using timeLockERC721()
  2. Eve transfers NFT to Bob using transferERC721() but without calling timeUnlockERC721() first
  3. Alice buys the same NFT back from Bob (e.g. because it is now considered rare and more valuable) and again puts it back in her vault but this time without locking/delegating it to any recipient i.e. intending to control it herself.
  4. Because this NFT's timelock data and delegate approval for Eve is never removed after Step 2, the NFT is still treated as timelocked in the vault with previous delegate Eve as the recipient (because of stale data in timelockERC721s and nftApprovals)
  5. Alice now cannot withdraw her own NFT without Eve’s help because the check on L510 will only allow Eve to transfer this NFT out of the vault.
  6. If Eve is no longer trusted/accessible then the NFT is locked in the vault forever.

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add “delete timelockERC721s[timelockERC721Keys[nftContract][i]];” after L510.

Double-checking Visor Finance form

Handle

sockdrawermoney

Vulnerability details

Impact

Detailed description of the impact of this finding.

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

Recommended Mitigation Steps

introduce a max lock time limit

Handle

paulius.eth

Vulnerability details

Impact

I suggest introducing a max lock time limit, so it won't be possible to accidentally lock tokens forever. Now there is no limit on when the timelock expires so theoretically it is possible to set it to hundreds of years which I think in practice wouldn't make sense.

Recommended Mitigation Steps

Even though this is more of a theoretical issue I think you should introduce a reasonable upper limit for the timelock period.

Incorrect and unnecessary logic in delegatedTransferERC20()

Handle

0xRajeev

Vulnerability details

Impact

delegatedTransferERC20() is meant to be called by delegates who have the required approvals from the vault owner to transfer ERC20s out of the vault. However, the logic implemented assumes that even vault owners can call this function for some reason. Vault owners can directly call transferERC20() without having to delegate to themselves and call delegatedTransferERC20().

Also, there is no need for the require on L449 for erc20Approvals because the subtraction of approvals on L459 will enforce the >= check on approvals and amount being transferred.

The impact is that owner calls to delegatedTransferERC20() might fail if they have not given themselves approvals as delegates. The impact is also wasted gas because of the extra check on owner if this is only meant to be called by non-owner delegates.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L442-L463

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L412-L432

Tools Used

Manual Analysis

Recommended Mitigation Steps

  1. Remove the owner check and approval check from L447 to L452: https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L447-L452

  2. Another option is to remove the separate transferERC20() and delegatedTransferERC20() functions and combine them similar to transferERC721(), which can manage both owner and delegate flows accordingly.

Timelock keys are never removed after unlocks

Handle

0xRajeev

Vulnerability details

Impact

timelockERC20Keys and timelockERC721Keys are used to keep track of number of timelocks for ERC20 and ERC721 tokens. While timelockERC20() and timelockERC721() functions update these data structures to add the new timelocks, the corresponding unlock functions do not remove the expired timelocks.

This results in their getter functions getTimeLockCount() and getTimeLockERC721Count() returning the number of all timelocks ever held instead of the expected number of timelocks that are currently active.

Proof of Concept

Let’s say 5 timelocks are creates for a specific ERC20 token of which 3 have been unlocked after expiry. The getter function getTimeLockCount() incorrectly reports 5 instead of 2.

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L82

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L92

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L550

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L608

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L247-L255

Tools Used

Manual Analysis

Recommended Mitigation Steps

Remove unlocked keys from timelockERC20Keys and timelockERC721Keys in timeUnlockERC20() and timeUnlockERC721() functions.

function transferERC721 does not reset nftApprovals if the msg.sender is a delegate

Handle

paulius.eth

Vulnerability details

Impact

When the sender is not the owner, the function transferERC721 checks that the sender has nftApprovals set to true. However, after doing the transfer, it does not reset it to false. Maybe that was intended here, but comparing to the function delegatedTransferERC20, it reduces erc20Approvals after the transfer so I expected to see something similar here.

Recommended Mitigation Steps

A possible solution:
if(msg.sender != _getOwner()) {
nftApprovals[keccak256(abi.encodePacked(msg.sender, nftContract, tokenId))] = false;
}

missing condition in addTemplate(bytes32 name, address template), visorFactory.sol

Handle

JMukesh

Vulnerability details

Impact

In function addTemplate(bytes32 name, address template) , in require we check given name has been allotted or not. it misses checking of second parameter of function that is template. without checking of template address , unintended address can be set for given name.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/main/contracts/contracts/visor/VisorFactory.sol#L27

Tools Used

manual review

Recommended Mitigation Steps

add one more condition in require() for checking of template address

Missing events

Handle

cmichel

Vulnerability details

Vulnerability Details

The following events are not used:

  • IInstanceRegistry.InstanceRemoved

Impact

Unused code can hint at programming or architectural errors.

Recommended Mitigation Steps

Use it or remove it.

Denial of service for NFT's

Handle

gpersoon

Vulnerability details

Impact

The function _removeNft uses more gas as more NFT's are added.
An attacker can send random NFT's to the contract, which are received via onERC721Received.
This functions adds the NFT's to the array nfts, using the function _addNft.
The longer the nfts array, the more gas is used by the function _removeNft, which is called from transferERC721 & timeUnlockERC721.
I tried the proof concept below in Remix, and noticed the required amount of gas increased with about 3000 per NFT.
So with enough NFT's you get an out of gas error in _removeNft and thus the functions transferERC721 & timeUnlockERC721 will no longer work.

Proof of Concept

function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata) external override returns (bytes4) {
_addNft(msg.sender, tokenId);
return IERC721Receiver.onERC721Received.selector;
}

// Test gas usage
pragma solidity ^0.8.0;
contract test {
struct Nft {
uint256 tokenId;
address nftContract;
}

Nft[] public nfts;

 function _addNft(address nftContract, uint256 tokenId) public {
  nfts.push(Nft({tokenId: tokenId,nftContract: nftContract}));
}

function _removeNft(address nftContract, uint256 tokenId) public {
  uint256 len = nfts.length;
  for (uint256 i = 0; i < len; i++) {
    Nft memory nftInfo = nfts[i];
    if (nftContract == nftInfo.nftContract && tokenId == nftInfo.tokenId) {
      if(i != len - 1) {
        nfts[i] = nfts[len - 1];
      }
      nfts.pop();
 
      break;
    }
  }
}    
function testadd(uint len) public {
    for (uint256 i = 0; i < len; i++) {
       _addNft(address(this),i);
    }
}    

}

Tools Used

Remix

Recommended Mitigation Steps

Use an alternative algorithm for _addNft and _removeNft where the lookup in _removeNft doesn't take O(n), but O(1)

Lack of zero-address check on token/ETH destination

Handle

0xRajeev

Vulnerability details

Impact

No zero-address check on token/ETH transfer destination 'to' address may lead to unintentional token/ETH burn/loss.

Proof of Concept

Visor contract uses Uniswap’s TransferHelper library functions which call the token’s transfer/transferFrom functions (or ETH destination account) and check return value for success and return data, but does not do any zero-address validation of the ‘to’ token/ETH transfer destination address.

Therefore, for tokens, unless the token contract implements a zero-address check on ‘to’ address like OpenZeppelin’s ERC20/ERC721 contracts, this may accidentally lead to token burn/loss. ETH transferred to zero address will be burned/lost.

Uniswap’s TransferHelper.safeTransfer: https://github.com/Uniswap/uniswap-lib/blob/c01640b0f0f1d8a85cba8de378cc48469fcfd9a6/contracts/libraries/TransferHelper.sol#L20-L31
https://github.com/Uniswap/uniswap-lib/blob/c01640b0f0f1d8a85cba8de378cc48469fcfd9a6/contracts/libraries/TransferHelper.sol#L33-L45

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L423

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L462

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L423

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L515

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L552

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L574

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L637

ETH: https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L472

Tools Used

Manual Analysis

Recommended Mitigation Steps

Perform zero-address validation on the destination ‘to’ address of token/ETH transfer before calling Uniswap’s TransferHelper library functions.

Incorrect event emitted

Handle

0xRajeev

Vulnerability details

Impact

In function timeLockERC721(), instead of emitting event TimeLockERC721, event TimeLockERC20 is incorrectly emitted here which will confuse user, off-chain monitoring and interfacing logic.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L553

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L99

Tools Used

Manual Analysis

Recommended Mitigation Steps

Replace emission of TimeLockERC20 with TimeLockERC721

Delegated transfer of owner fails

Handle

cmichel

Vulnerability details

Vulnerability Details

The Visor.delegatedTransferERC20 function skips the approval check if msg.sender == _getOwner(), however, it will still try to reduce the approval in that case.
As it is implemented that the owner does not need an approval for this function, a previous approve action was most likely never sent and the transaction will fail when trying to reduce the erc20Approvals field by the amount due to the usage of SafeMath underflow checks.

Impact

Owners cannot transfer using this function.

Recommended Mitigation Steps

Move the approval subtraction to the if path.

delegatedTransferERC20 can revert when called by owner

Handle

gpersoon

Vulnerability details

Impact

If the function delegatedTransferERC20 is called from the owner (e.g. msg.sender == _getOwner ) then
erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] doesn't have to set, so it can have the value of 0.

If you then subtract the amount, you will get an error and the code will revert:
erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] = erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))].sub(amount);

A work around would be to call approveTransferERC20 also for the owner.

Proof of Concept

function delegatedTransferERC20(address token,address to,uint256 amount) external {
if(msg.sender != _getOwner()) {
require(erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] >= amount,"Account not approved to transfer amount");
}
// check for sufficient balance
require(IERC20(token).balanceOf(address(this)) >= (getBalanceLocked(token).add(amount)).add(timelockERC20Balances[token]),"UniversalVault: insufficient balance");

    erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] = erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))].sub(amount);
    
    // perform transfer
    TransferHelper.safeTransfer(token, to, amount);
}

Tools Used

Editor

Recommended Mitigation Steps

Also add
if(msg.sender != _getOwner())
before
erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] = erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))].sub(amount);

Hypervisor.stake does not transfer tokens

Handle

cmichel

Vulnerability details

Vulnerability Details

The Hypervisor's stake action states:

token transfer: transfer staking tokens from msg.sender to vault

But no tokens are ever transferred.

Impact

Anyone with a permission can lock any amount of tokens.

Recommended Mitigation Steps

Transfer the tokens or clarify how this comment is supposed to be understood.

Missing parameter validation

Handle

cmichel

Vulnerability details

Vulnerability Details

Some parameters of functions are not checked:

  • VisorFactory.createSelected: should check that templates[name] != 0

Impact

Wrong parameters can lead to unintended behaviour and extensive gas cost.

Recommended Mitigation Steps

Validate the parameters.

getNftById is querying against the index not id

Handle

paulius.eth

Vulnerability details

Impact

getNftById is actually 'get NFT by index' as it queries the element from the array by index, not by tokenId. The index may not always equal id as _addNft does not automatically assign index incrementally but rather use a parameter's value. Same with getNftIdByTokenIdAndAddr, it returns index, not token id.

Recommended Mitigation Steps

Either rename functions to distinguish between id and index or refactor the function to suit its name.

Breaking out of loop can save gas

Handle

0xRajeev

Vulnerability details

Impact

In function transferERC721(), the for loop iterates over all the time locked keys for the nftContract timelockERC721Keys[nftContract].length times. Given that there will only be a maximum of one tokenID that will match (because of unique NFT tokenIDs), if any, we can break from iterating the rest of the loop after a match on L505 and the checks within the if body. This will prevent iterating the rest of the loop and trying to match the if condition on L505 after a match has already happened.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L505-L511

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add a break statement after L510 within the if body.

Transaction-Order-Dependence race condition for approveTransferERC20()

Handle

0xRajeev

Vulnerability details

Impact

Similar to ERC20 approve() being susceptible to double-spend allowance due to front-running, approveTransferERC20() here is also susceptible. For reference, see https://swcregistry.io/docs/SWC-114.

This is the classic ERC20 approve() race condition where a malicious spender can double-spend allowance (old and new allowance) by front-running the owner’s second approve() call that aims to change the allowance.

The impact is double-spend of approved tokens which leads to loss of owner’s funds when owner tries to change spender’s allowance.

Proof of Concept

Assume that Alice has approved Eve to transfer n of her tokens, then Alice decides to change Eve's approval to m tokens. Alice submits a function call to approve with the value m for Eve. Eve monitors the Ethereum mempool and so knows that Alice is going to change her approval to m. Eve immediately submits a transferFrom request sending n of Alice's tokens to herself, but gives it a much higher gas price than Alice's transaction to front-run it. The transferFrom executes first so gives Eve n tokens and sets Eve's approval to zero. Then Alice's transaction executes and sets Eve's approval to m tokens. Eve then sends those m tokens to herself as well. Thus Eve gets n + m tokens even thought she should have gotten at most max(n,m).

Expected order where Eve can only transfer 50 tokens:

  1. Alice: approveTransferERC20(token, Eve, 100)
  2. Alice: approveTransferERC20(token, Eve, 50)
  3. Eve: delegatedTransferERC20(token, Eve, 100) -> Fails
  4. Eve: delegatedTransferERC20(token, Eve, 50) -> Passes

Exploit order where Eve can transfer 150 tokens:

  1. Alice: approveTransferERC20(token, Eve, 100)
  2. Eve: delegatedTransferERC20(token, Eve, 100) -> Passes
  3. Alice: approveTransferERC20(token, Eve, 50)
  4. Eve: delegatedTransferERC20(token, Eve, 50) -> Passes

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L426-L432

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L434-L463

Tools Used

Manual Analysis

Recommended Mitigation Steps

Provide functions to increase and decrease allowance instead of approves for only setting specific amounts.

Same keccak256 evaluated 3x

Handle

gpersoon

Vulnerability details

Impact

The function delegatedTransferERC20 evaluated the following statement 3x:
keccak256(abi.encodePacked(msg.sender, token)

keccak256 is a relatively expensive operation, so calling it 3x with the same value costs more gas than neccesary.

Proof of Concept

function delegatedTransferERC20(
    address token,
    address to,
    uint256 amount
) external {
    if(msg.sender != _getOwner()) {

    require( 
        erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] >= amount,
        "Account not approved to transfer amount"); 
    } 

    // check for sufficient balance
    require(
        IERC20(token).balanceOf(address(this)) >= (getBalanceLocked(token).add(amount)).add(timelockERC20Balances[token]),
        "UniversalVault: insufficient balance"
    );
    erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] = erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))].sub(amount);
    
    // perform transfer
    TransferHelper.safeTransfer(token, to, amount);
}

Tools Used

Editor

Recommended Mitigation Steps

Store the value of keccak256(abi.encodePacked(msg.sender, token) in a temporary variable.

timelockERC721Keys are not deleted after unlocking the token

Handle

paulius.eth

Vulnerability details

Impact

This is problematic because the same token can be locked several times in the future with different recipients thus making the function transferERC721 unusable. This function iterates over all the timelockERC721Keys and checks that the expires <= block.timestamp and recipient == msg.sender, however, if there are more than 1 entry for the same token (e.g. one old and one new) but with different recipients, this function will always fail. Also, it is very gas-consuming as it does not break after finding the first occurrence in the loop.

Recommended Mitigation Steps

Probably the best solution would be to delete timelockERC721Keys after unlocking actions or introduce a new boolean flag (unlocked) that marks already unlocked tokens so that iterations can skip them.

Not reverting on failing ERC20 transfer

Handle

Sherlock

Vulnerability details

Impact

In the Visor.sol contract an IERC20 transferFrom() with an arbitrary token argument is used. The return value of the transferFrom() is not used, a boolean indicating if the transaction succeeded. Meaning the transaction could succeed without the actual transfer of tokens happening.

The new entry the time lock is valid even though the tokens are not transferred. User could drain tokens in the contract (e.g. from other users who's transfer did succeed)

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/main/contracts/contracts/visor/Visor.sol#L610

Recommended Mitigation Steps

Use the OpenZeppelin SafeERC20 contract. Changing the call to safeTransferFrom(). This not only makes the transaction revert if the returned boolean is false. But also handles non-standard ERC20 tokens that don’t have boolean return values.

In Visor's case, I recommend using the TransferHelper.safeTransferFrom() as this library is used in other parts of the code.

function timeLockERC20 does not check the return value of transferFrom

Handle

paulius.eth

Vulnerability details

Impact

function timeLockERC20 uses transferFrom for erc20 transfers, however, it does not check the return value. According to the ERC20 standard, this function should return a boolean to indicate success. Not checking that may not work with some tokens that, for example, return false if the transfer fails.

Recommended Mitigation Steps

I see 2 mitigation options:

  1. Use safeTransferFrom
  2. check balanceOf before and after and ensure that balanceAfter >= balanceBefore.add(amount).

NFT transfer approvals are not removed and cannot be revoked thus leading to loss of NFT tokens

Handle

0xRajeev

Vulnerability details

Impact

NFT transfer approvals that are set to true in approveTransferERC721() are never set to false and there is no way to remove such an nft approval.

Impact-1: The approval is not removed (set to false) after a transfer in transferERC721(). So if the NFT is ever moved back into the owner's vault again, then the previous/compromised delegate can again transfer it to any address of choice without requiring a new approval.

Impact-2: If a delegate becomes compromised/untrustworthy after granting approval but before transfer then the owner will lose its NFT because there is no mechanism to revoke the approval that was granted earlier.

Proof of Concept

PoC-1:
Alice grants Eve approval to transfer a particular NFT out of its vault using approveTransferERC721()
Eve, who has transfer rights to that NFT from Alice’s vault, transfers that NFT to Bob using transferERC721()
Alice decides to buy back that NFT (e.g. because it is now considered rare and more valuable) from Bob and transfers it back to its vault
Eve, who continues to have transfer rights to that NFT from Alice’s vault, can steal that NFT and transfer to anyone

PoC-2:
Alice grants Eve approval to transfer a particular NFT out of its vault using approveTransferERC721()
Alice learns that Eve’s keys are compromises or that Eve is malicious and wants to revoke the approval but there is no mechanism to do so
Eve (or whoever stole her credentials) has transfer rights to that NFT from Alice’s vault and can steal that NFT and transfer to anyone

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L477-L487

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L489-L522

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add a boolean parameter to approveTransferERC721() and set the nftApprovals to that parameter which can be true for giving approval and false for removing/revoking approval
If msg.sender != _getOwner(), call approveTransferERC721() with the boolean false to remove approval before making a transfer in transferERC721() on L515.

Can lock more tokens than in contract

Handle

cmichel

Vulnerability details

Vulnerability Details

The Visor.timeLockERC20 allows locking any amount of tokens exceeding the contract's token balance.

Impact

The recipient might think that they'll receive the tokens after expiry but it could be that the contract is already out of tokens by then.

Recommended Mitigation Steps

Make sure that the contract has enough tokens to cover all locks at all times.

nftApprovals not reset in transferERC721

Handle

gpersoon

Vulnerability details

Impact

The function transferERC721 checks for nftApprovals, however it never reset the value of nftApprovals.
This means if the NFT would end up in the contract again in the future, it could be transferred again without an explicit approval.

Proof of Concept

function transferERC721(
address to,
address nftContract,
uint256 tokenId
) external {
if(msg.sender != _getOwner()) {
require( nftApprovals[keccak256(abi.encodePacked(msg.sender, nftContract, tokenId))], "NFT not approved for transfer");
}
....
_removeNft(nftContract, tokenId);
IERC721(nftContract).safeTransferFrom(address(this), to, tokenId);
}

Tools Used

Editor

Recommended Mitigation Steps

Set nftApprovals[keccak256(abi.encodePacked(msg.sender, nftContract, tokenId))] = 0

Vault factory owner can frontrun vault creators

Handle

cmichel

Vulnerability details

Risk Rating

low

Vulnerability Details

VisorFactory.create/create2 uses the activeTemplate as a template which can be changed by the owner using setActive.

Impact

A user might check the current activeTemplate and decides to deploy this specific template.
The owner can then frontrun this transaction and an undesired template is deployed which the user might not even notice.

Recommended Mitigation Steps

Remove the functions working with activeTemplate and use the functions that explicitly define a template instead.
There's no need to have an active template.

function delegatedTransferERC20 reduces the erc20Approvals by amount even when the sender is an owner

Handle

paulius.eth

Vulnerability details

Impact

When the msg.sender is not an owner, delegatedTransferERC20 requires that the sender has erc20Approvals >= amount and later reduces it:
erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] = erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))].sub(amount);
However, this reduce is applied no matter if the sender is an owner or a delegate so owner will also need to be approved enough tokens or otherwise this calculation will fail.

Recommended Mitigation Steps

As you probably don't want to grant approveTransferERC20 for very token to the owner, I suggest to solve this by reducing the amount only if the sender is not an owner:
if(msg.sender != _getOwner()) {
erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))] = erc20Approvals[keccak256(abi.encodePacked(msg.sender, token))].sub(amount);
}

Also, this contest does not ask for gas optimizations but calculating and storing in variables upfront can significantly reduce the gas that users need to pay to use your app in such cases:
keccak256(abi.encodePacked(msg.sender, token))

function transferERC721 does not delete timelockERC721s if the token was among locked tokens

Handle

paulius.eth

Vulnerability details

Impact

function timeUnlockERC721 deletes timelockERC721s after removing NFT, so I expect a similar behavior with function transferERC721. It iterates over timelockERC721Keys and if it finds the token among locked tokens, it does some extra checks and later removes this token but does not delete timelockERC721s.

Recommended Mitigation Steps

Solution: delete timelockERC721s possibly in the for loop if you find the right token.

Lack of address input validation will lock tokens in contract

Handle

0xRajeev

Vulnerability details

Impact

Functions timeLockERC721() and timeLockERC20() are used by the vault owner to timelock tokens in the vault with a specified recipient address as the only one with the right to withdraw after timelock expiry.

If a zero/incorrect recipient address is used here accidentally, the ERC20/ERC721 tokens will be locked in contract forever because no one else can withdraw them.

Proof of Concept

PoC-1: Alice, the vault owner, intending to specify Bob’s address as the timelock recipient specifies Eve’s address. Given that there is no way to cancel/correct/override this accident, Eve ends up gaining those locked tokens after timelock expiry.

PoC-2: Alice, the vault owner, intending to specify Bob’s address as the timelock recipient makes a typo in Bob’s address to specify an address for which she does not have a private key. Given that there is no way to cancel/correct/override this accident, those tokens after timelock expiry end up locked in vault forever.

PoC-3: Alice, the vault owner, intending to specify Bob’s address as the timelock recipient makes a mistake in transaction specification to specify a zero address (for which no one has a private key). Given that there is no way to cancel/correct/override this accident, those tokens after timelock expiry end up locked in vault forever.

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L524-L554

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L578-L612

Tools Used

Manual Analysis

Recommended Mitigation Steps

Consider adding a correction window timer of say 1 block within which the vault owner is allowed to call timeLockERC721()/timeLockERC20() once more with a different recipient in case they specified an incorrect address the first time.

Unbounded loops

Handle

paulius.eth

Vulnerability details

Impact

Unbounded for loops may exceed gas limit. There are several places where iterations over dynamically sized arrays take place. For example, function _removeNft iterates over all the NFTs and tries to find the one that is needed to be removed. However, iterating over a dynamically sized array is dangerous because if there are too many items stored it may exceed block gas limit before reaching the right element. It may not be impossible to remove the NFT if the nfts array becomes too large. Same issue is possible with getBalanceLocked that iterates over the unbounded list of _lockSet. If this list grows too large, it may become impossible to execute functions that use it (transferERC20, delegatedTransferERC20, timeUnlockERC20).

Recommended Mitigation Steps

Refactor to use mappings instead of arrays (or store indexes separately) or introduce size limits on arrays that will prevent such cases.

rageQuit doesn't support all functionality

Handle

gpersoon

Vulnerability details

Impact

rageQuit only supports the standard locking of ERC20 tokens.
It doesn't support the timelocks on ERC20 tokens.
There is also no equivalent rageQuit function for ERC721 tokens.

This also means the timelock datastructures for ERC20 are not cleaned when a rageQuit is done. Possibly allowing an unexpected timeUnlockERC20 in the future.

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

Editor

Recommended Mitigation Steps

Check if rageQuit is also relevant for timelocks on ERC20 tokens and/or ERC721 tokens.
If this is the case add the required functionality

transferERC721 doesn't clean timelockERC721s

Handle

gpersoon

Vulnerability details

Impact

The function transferERC721 works similar to the functions timeUnlockERC721 with timelocked NFT's.
However timeUnlockERC721 cleans timelockERC721s (delete timelockERC721s[key];), while transferERC721 doesn't clean timelockERC721s

This could mean that timelock keys could be used later on (when the NFT would have been transfered to the contract on a later moment in time).
Also the administration doesn't correspond to the available NFT's.
Additionally doing a delete gives backs some gas (at least for now).

Proof of Concept

function transferERC721(
address to,
address nftContract,
uint256 tokenId
) external {
if(msg.sender != _getOwner()) {
require( nftApprovals[keccak256(abi.encodePacked(msg.sender, nftContract, tokenId))], "NFT not approved for transfer");
}

    for(uint256 i=0; i<timelockERC721Keys[nftContract].length; i++) {
      if(tokenId == timelockERC721s[timelockERC721Keys[nftContract][i]].tokenId) {
          require(
            timelockERC721s[timelockERC721Keys[nftContract][i]].expires <= block.timestamp, 
            "NFT locked and not expired"
          );
          require( timelockERC721s[timelockERC721Keys[nftContract][i]].recipient == msg.sender, "NFT locked and must be withdrawn by timelock recipient");
      }
    }

    _removeNft(nftContract, tokenId);
    IERC721(nftContract).safeTransferFrom(address(this), to, tokenId);
}

function timeUnlockERC721(address recipient, address nftContract, uint256 tokenId, uint256 expires) public {

  bytes32 key = keccak256(abi.encodePacked(recipient, nftContract, tokenId, expires)); 
  require(
    timelockERC721s[key].expires <= block.timestamp,
    "Not expired yet"
  );

  require(msg.sender == timelockERC721s[key].recipient, "Not recipient");

  _removeNft(nftContract, tokenId);
  delete timelockERC721s[key];

  IERC721(nftContract).safeTransferFrom(address(this), recipient, tokenId);
  emit TimeUnlockERC721(recipient, nftContract, tokenId, expires);
}

Tools Used

Editor

Recommended Mitigation Steps

Check if the timelockERC721s mapping should also be cleaned from transferERC721, if so adapt the code accordingly.

Approval for NFT transfers is not removed after transfer

Handle

cmichel

Vulnerability details

Vulnerability Details

The Visor.transferERC721 does not reset the approval for the NFT.

Impact

An approved delegatee can move the NFT out of the contract once.
It could be moved to a market and bought by someone else who then deposits it again to the same vault.
The first delegatee can steal the NFT and move it out of the contract a second time.

Recommended Mitigation Steps

Reset the approval on transfer.

Unbounded for-loop bricks transferERC721()

Handle

toastedsteaksandwich

Vulnerability details

Impact

The nfts array in the Visor contract could become overpopulated, causing certain functions that loop over it to brick, due to the gas limit. These functions include transferERC721() and getNftIdByTokenIdAndAddr(). The severity of this issue is increased as the onERC721Received() function was missing validation as to whether or not NFTs were actually received, making it easier to exploit this vulnerability.

Proof of Concept

The proof of concept code can be found here: https://gist.github.com/toastedsteaksandwich/0443dee7b3db7c9a31a3ede92680e777

The following is included:

1 - A brickpoc.js file, which includes a test case showing that the transferERC721 works, and then filling the nfts array with bogus entries to show that the function becomes bricked through a "run out of gas" revert. The gas limit used in the hardhat config is 15m, roughly equal to the current gas limit. The revert is caused by the _removeNft() function attempting to run through the nfts array.

2 - The sample NFT used - as from https://docs.alchemy.com/alchemy/tutorials/how-to-create-an-nft/how-to-mint-a-nft

3 - The output from the unit tests.

4 - An updated transferERC721() function - I've slightly modified the transferERC721() function to make the POC easier to produce - I've included the updated function here in the gist for transparency. The modification doesn't affect the vulnerability, it removes access control and locking validation while leaving in the removal of the NFT entry (_removeNft()), which is the issue.

Tools Used

Hardhat with the gas-reporter tool.

Recommended Mitigation Steps

To mitigate this issue, the use of an unbounded for-loop should be avoided. This can be done by using a mapping of nftContract=>tokenId=>bool to indicate ownership, instead of using the nfts array.

The onERC721Received() function should also be patched to validate whether or not an NFT was actually received. This can be done by validating ownership through the NFT contract and validating that the mapping has not yet been updated. This will avoid representation issues in the code (e.g. having NFT x in the Visor state, but not according to the associated NFT contract).

Use a temporary variable to cache repetitive storage reads

Handle

0xRajeev

Vulnerability details

Impact

In function transferERC721(), the array value stored in a mapping timelockERC721Keys[nftContract][i] is read three times in three different places within the loop iteration. This consumes a lot of unnecessary gas because SLOADs are expensive. This can be prevented by saving the value timelockERC721Keys[nftContract][i] in a temporary bytes32 variable at the beginning of the iteration and using that instead.

Proof of Concept

Declaration: https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L82

Use 1: https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L505

Use 2: https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L507

Use 3: https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L510

Tools Used

Manual Analysis

Recommended Mitigation Steps

Save the value timelockERC721Keys[nftContract][i] in a temporary bytes32 variable at the beginning of the iteration and using that instead

Unbounded iteration

Handle

cmichel

Vulnerability details

Vulnerability Details

The Visor._removeNft iterates over all nfts.
Anyone can add to this array by depositing NFTs, see Visor.onERC721Received.

Other occurences that makes an unbounded iteration over arrays:

  • Visor.getBalanceLocked
  • Visor.getNftIdByTokenIdAndAddr
  • HyperVisor.isValidVault
  • HyperVisor.calculateTotalStakeUnits

Impact

The transactions can fail if the arrays get to big and the transaction would consume more gas than the block limit.
This will then result in a denial of service for the desired functionality and break core functionality.

Recommended Mitigation Steps

Keep the arrays small by setting a max size or make it possible to process the arrays in chunks in several transactions if possible.

bug title

Handle

ninek

Vulnerability details

Impact

Detailed description of the impact of this finding.

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

Recommended Mitigation Steps

lock/unlock signatures may be replayed on a different contract/chain

Handle

0xRajeev

Vulnerability details

Impact

lock() and unlock() functions can be called by delegate with signed permission from owner. The permission signature includes a nonce which prevents replay attacks over time. However, it neither includes this contract’s address nor the chain ID.

The impact is that if this contract is ever redeployed on the same mainnet at a different address for some reason or deployed on a Layer 2 (Optimism, Arbitrum, Polygon etc.) or any other EVM-compatible chain, the previously used signature may be replayed on the new chain leading to unexpected outcomes.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/dda9be0bcbf4b2c549608405ccdf9469bc2cbaf1/contracts/contracts/visor/Visor.sol#L277

https://github.com/code-423n4/2021-05-visorfinance/blob/dda9be0bcbf4b2c549608405ccdf9469bc2cbaf1/contracts/contracts/visor/Visor.sol#L327

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add address(this) and chain ID to the signed permission.

Unused state variable and associated setter function

Handle

0xRajeev

Vulnerability details

Impact

The uri state variable is never used anywhere but has an associated setter function setURI(). Removing the state variable and associated setter function will save both storage slot and contract deployment cost because of reduced size.

Proof of Concept

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L63

https://github.com/code-423n4/2021-05-visorfinance/blob/e0f15162a017130aa66910d46c70ee074b64dd40/contracts/contracts/visor/Visor.sol#L400-L402

Tools Used

Manual Analysis

Recommended Mitigation Steps

Remove unused state variable and associated setter function or add missing code to use them.

setURI / uri not used

Handle

gpersoon

Vulnerability details

Impact

The variable uri (which is set in the function setURI) is not used elsewhere in Visor.sol.
Although it is accessible from the outside because its a public variable.

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

Editor

Recommended Mitigation Steps

Check if uri is used elsewhere and is useful, it not remove the code for setURI and uri.

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.