-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Carbon batch router #175
base: dev
Are you sure you want to change the base?
Carbon batch router #175
Changes from all commits
efc4d47
207b80d
3cef9a1
d582f10
6510815
adebb44
62b429c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.19; | ||
|
||
import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | ||
import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; | ||
import { Address } from "@openzeppelin/contracts/utils/Address.sol"; | ||
|
||
import { ICarbonController } from "../carbon/interfaces/ICarbonController.sol"; | ||
import { IVoucher } from "../voucher/interfaces/IVoucher.sol"; | ||
|
||
import { Upgradeable } from "./Upgradeable.sol"; | ||
import { Order } from "../carbon/Strategies.sol"; | ||
import { Utils } from "../utility/Utils.sol"; | ||
import { Token } from "../token/Token.sol"; | ||
|
||
struct StrategyData { | ||
Token[2] tokens; | ||
Order[2] orders; | ||
} | ||
|
||
/** | ||
* @dev Contract to batch create carbon controller strategies | ||
*/ | ||
contract CarbonBatcher is Upgradeable, Utils, ReentrancyGuard, IERC721Receiver { | ||
using Address for address payable; | ||
|
||
error InsufficientNativeTokenSent(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this error move to common/shared utils? |
||
|
||
ICarbonController private immutable carbonController; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
IVoucher private immutable voucher; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
/** | ||
* @dev triggered when tokens have been withdrawn from the carbon batcher | ||
*/ | ||
event FundsWithdrawn(Token indexed token, address indexed caller, address indexed target, uint256 amount); | ||
|
||
constructor( | ||
ICarbonController _carbonController, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't prefix function arguments with an |
||
IVoucher _voucher | ||
) validAddress(address(_carbonController)) validAddress(address(_voucher)) { | ||
carbonController = _carbonController; | ||
voucher = _voucher; | ||
_disableInitializers(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing new line? |
||
} | ||
|
||
/** | ||
* @dev fully initializes the contract and its parents | ||
*/ | ||
function initialize() public initializer { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can it be |
||
__CarbonBatcher_init(); | ||
} | ||
|
||
// solhint-disable func-name-mixedcase | ||
|
||
/** | ||
* @dev initializes the contract and its parents | ||
*/ | ||
function __CarbonBatcher_init() internal onlyInitializing { | ||
__Upgradeable_init(); | ||
} | ||
|
||
/** | ||
* @inheritdoc Upgradeable | ||
*/ | ||
function version() public pure virtual override(Upgradeable) returns (uint16) { | ||
return 1; | ||
} | ||
|
||
/** | ||
* @notice creates several new strategies, returns the strategies id's | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
* | ||
* requirements: | ||
* | ||
* - the caller must have approved the tokens with assigned liquidity in the orders | ||
*/ | ||
function batchCreate( | ||
StrategyData[] calldata strategies | ||
) external payable greaterThanZero(strategies.length) nonReentrant returns (uint256[] memory) { | ||
uint256[] memory strategyIds = new uint256[](strategies.length); | ||
uint256 txValueLeft = msg.value; | ||
|
||
// extract unique tokens and amounts | ||
(Token[] memory uniqueTokens, uint256[] memory amounts) = _extractUniqueTokensAndAmounts(strategies); | ||
// transfer funds from user for strategies | ||
for (uint256 i = 0; i < uniqueTokens.length; i = uncheckedInc(i)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps it's time to upgrade to Solidity >0.8.22? |
||
Token token = uniqueTokens[i]; | ||
uint256 amount = amounts[i]; | ||
if (token.isNative()) { | ||
if (txValueLeft < amount) { | ||
revert InsufficientNativeTokenSent(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check isn't strictly needed due to the checked subtraction below |
||
} | ||
txValueLeft -= amount; | ||
continue; | ||
} | ||
token.safeTransferFrom(msg.sender, address(this), amount); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These can be in an |
||
_setCarbonAllowance(token, amount); | ||
} | ||
|
||
// create strategies and transfer nfts to user | ||
for (uint256 i = 0; i < strategies.length; i = uncheckedInc(i)) { | ||
// get tokens for this strategy | ||
Token[2] memory tokens = strategies[i].tokens; | ||
Order[2] memory orders = strategies[i].orders; | ||
// if any of the tokens is native, send this value with the create strategy tx | ||
uint256 valueToSend = 0; | ||
if (tokens[0].isNative()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can cache |
||
valueToSend = orders[0].y; | ||
} else if (tokens[1].isNative()) { | ||
valueToSend = orders[1].y; | ||
} | ||
|
||
// create strategy on carbon | ||
strategyIds[i] = carbonController.createStrategy{ value: valueToSend }(tokens[0], tokens[1], orders); | ||
// transfer nft to user | ||
voucher.safeTransferFrom(address(this), msg.sender, strategyIds[i], ""); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm.. maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can cache |
||
} | ||
// refund user any remaining native token | ||
if (txValueLeft > 0) { | ||
// forwards all available gas | ||
payable(msg.sender).sendValue(txValueLeft); | ||
} | ||
|
||
return strategyIds; | ||
} | ||
|
||
/** | ||
* @notice withdraws funds held by the contract and sends them to an account | ||
* | ||
* requirements: | ||
* | ||
* - the caller is admin of the contract | ||
*/ | ||
function withdrawFunds( | ||
Token token, | ||
address payable target, | ||
uint256 amount | ||
) external validAddress(target) onlyAdmin nonReentrant { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this function required? |
||
if (amount == 0) { | ||
return; | ||
} | ||
|
||
// forwards all available gas in case of ETH | ||
token.unsafeTransfer(target, amount); | ||
|
||
emit FundsWithdrawn({ token: token, caller: msg.sender, target: target, amount: amount }); | ||
} | ||
|
||
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing NATSPEC? |
||
return IERC721Receiver.onERC721Received.selector; | ||
} | ||
|
||
/** | ||
* @dev extracts unique tokens and amounts for each token from the strategy data | ||
*/ | ||
function _extractUniqueTokensAndAmounts( | ||
StrategyData[] calldata strategies | ||
) private pure returns (Token[] memory uniqueTokens, uint256[] memory amounts) { | ||
// Maximum possible unique tokens | ||
Token[] memory tempUniqueTokens = new Token[](strategies.length * 2); | ||
uint256[] memory tempAmounts = new uint256[](strategies.length * 2); | ||
uint256 uniqueCount = 0; | ||
|
||
for (uint256 i = 0; i < strategies.length; i = uncheckedInc(i)) { | ||
StrategyData calldata strategy = strategies[i]; | ||
|
||
for (uint256 j = 0; j < 2; j = uncheckedInc(j)) { | ||
Token token = strategy.tokens[j]; | ||
uint128 amount = strategy.orders[j].y; | ||
|
||
// Check if the token is already in the uniqueTokens array | ||
uint256 index = _findInArray(token, tempUniqueTokens, uniqueCount); | ||
if (index == type(uint256).max) { | ||
// If not found, add to the array | ||
tempUniqueTokens[uniqueCount] = token; | ||
tempAmounts[uniqueCount] = amount; | ||
uniqueCount++; | ||
} else { | ||
// If found, aggregate the amount | ||
tempAmounts[index] += amount; | ||
} | ||
} | ||
} | ||
|
||
// Resize the arrays to fit the unique count | ||
uniqueTokens = new Token[](uniqueCount); | ||
amounts = new uint256[](uniqueCount); | ||
|
||
for (uint256 i = 0; i < uniqueCount; i = uncheckedInc(i)) { | ||
uniqueTokens[i] = tempUniqueTokens[i]; | ||
amounts[i] = tempAmounts[i]; | ||
} | ||
|
||
return (uniqueTokens, amounts); | ||
} | ||
|
||
function _findInArray(Token element, Token[] memory array, uint256 arrayLength) private pure returns (uint256) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing NATSPEC |
||
for (uint256 i = 0; i < arrayLength; i = uncheckedInc(i)) { | ||
if (array[i] == element) { | ||
return i; | ||
} | ||
} | ||
return type(uint256).max; // Return max value if not found | ||
} | ||
|
||
/** | ||
* @dev set carbon controller allowance to 2 ** 256 - 1 if it's less than the input amount | ||
*/ | ||
function _setCarbonAllowance(Token token, uint256 inputAmount) private { | ||
if (token.isNative()) { | ||
return; | ||
} | ||
uint256 allowance = token.toIERC20().allowance(address(this), address(carbonController)); | ||
if (allowance < inputAmount) { | ||
// increase allowance to the max amount if allowance < inputAmount | ||
token.forceApprove(address(carbonController), type(uint256).max); | ||
} | ||
} | ||
|
||
function uncheckedInc(uint256 i) private pure returns (uint256 j) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing NATSPEC There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should it moved to a common/shared utilities? |
||
unchecked { | ||
j = i + 1; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { DeployedContracts, deployProxy, InstanceName, setDeploymentMetadata } from '../../../utils/Deploy'; | ||
import { DeployFunction } from 'hardhat-deploy/types'; | ||
import { HardhatRuntimeEnvironment } from 'hardhat/types'; | ||
|
||
const func: DeployFunction = async ({ getNamedAccounts }: HardhatRuntimeEnvironment) => { | ||
const { deployer } = await getNamedAccounts(); | ||
|
||
const carbonController = await DeployedContracts.CarbonController.deployed(); | ||
const voucher = await DeployedContracts.Voucher.deployed(); | ||
|
||
await deployProxy({ | ||
name: InstanceName.CarbonBatcher, | ||
from: deployer, | ||
args: [carbonController.address, voucher.address] | ||
}); | ||
|
||
return true; | ||
}; | ||
|
||
export default setDeploymentMetadata(__filename, func); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { CarbonBatcher, ProxyAdmin } from '../../../components/Contracts'; | ||
import { DeployedContracts, describeDeployment } from '../../../utils/Deploy'; | ||
import { expect } from 'chai'; | ||
import { ethers } from 'hardhat'; | ||
|
||
describeDeployment(__filename, () => { | ||
let proxyAdmin: ProxyAdmin; | ||
let carbonBatcher: CarbonBatcher; | ||
|
||
beforeEach(async () => { | ||
proxyAdmin = await DeployedContracts.ProxyAdmin.deployed(); | ||
carbonBatcher = await DeployedContracts.CarbonBatcher.deployed(); | ||
}); | ||
|
||
it('should deploy and configure the carbon batcher contract', async () => { | ||
expect(await proxyAdmin.getProxyAdmin(carbonBatcher.address)).to.equal(proxyAdmin.address); | ||
expect(await carbonBatcher.version()).to.equal(1); | ||
}); | ||
|
||
it('carbon batcher implementation should be initialized', async () => { | ||
const implementationAddress = await proxyAdmin.getProxyImplementation(carbonBatcher.address); | ||
const carbonBatcherImpl: CarbonBatcher = await ethers.getContractAt('CarbonBatcher', implementationAddress); | ||
// hardcoding gas limit to avoid gas estimation attempts (which get rejected instead of reverted) | ||
const tx = await carbonBatcherImpl.initialize({ gasLimit: 6000000 }); | ||
await expect(tx.wait()).to.be.reverted; | ||
}); | ||
|
||
it('cannot call postUpgrade on carbon batcher', async () => { | ||
// hardcoding gas limit to avoid gas estimation attempts (which get rejected instead of reverted) | ||
const tx = await carbonBatcher.postUpgrade(true, '0x', { gasLimit: 6000000 }); | ||
await expect(tx.wait()).to.be.reverted; | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing new line?