Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
id: test
env:
BASE_RPC_URL: "${{ secrets.BASE_RPC_URL }}"
BSC_RPC_URL: "${{ secrets.BSC_RPC_URL }}"
BASE_UNISWAP_ROUTER: "${{ vars.BASE_UNISWAP_ROUTER }}"
BASE_POOL_MANAGER: "${{ vars.BASE_POOL_MANAGER }}"
BASE_POSITION_MANAGER: "${{ vars.BASE_POSITION_MANAGER }}"
Expand All @@ -60,3 +61,5 @@ jobs:
PERMIT2: "${{ vars.PERMIT2 }}"
WETH: "${{ vars.WETH }}"
AERODROME_ROUTER: "${{ vars.AERODROME_ROUTER }}"
PANCAKE_SMART_ROUTER: "${{ vars.PANCAKE_SMART_ROUTER }}"
PANCAKE_SMART_ROUTER_BSC: "${{ vars.PANCAKE_SMART_ROUTER_BSC }}"
20 changes: 10 additions & 10 deletions script/AddGloriaAndOtherDistributions.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ contract AddGloriaAndOtherDistributions is Script {
address aitvTokenAddr = vm.envAddress("AITV_TOKEN_BASE");
address gloriaTokenAddr = 0x3B313f5615Bbd6b200C71f84eC2f677B94DF8674;
address eolasTokenAddr = 0xF878e27aFB649744EEC3c5c0d03bc9335703CFE3;
address nimpetTokenAddr = 0x2a06A17CBC6d0032Cac2c6696DA90f29D39a1a29;
address sprotoTokenAddr = 0x2a06A17CBC6d0032Cac2c6696DA90f29D39a1a29;
address pettbroTokenAddr = 0x02D4f76656C2B4f58430e91f8ac74896c9281Cb9;

function run() public {
proposeGloriaPoolConfig();
proposeEolasPoolConfig();
proposeNimpetPoolConfig();
proposeSprotoPoolConfig();
deployGloriaDistribution();
deployEolasDistribution();
deployNimpetDistribution();
deploySprotoDistribution();
}

function proposeGloriaPoolConfig() public returns (uint256) {
Expand Down Expand Up @@ -137,15 +137,15 @@ contract AddGloriaAndOtherDistributions is Script {
return distributionId;
}

function proposeNimpetPoolConfig() public returns (uint256) {
function proposeSprotoPoolConfig() public returns (uint256) {
TokenDistributor distributor = TokenDistributor(payable(distributorAddr));

// Nimpet => USDC - Uniswap V3
// Sproto => USDC - Uniswap V3
vm.startBroadcast();
uint256 configId = distributor.proposePoolConfig(
PoolConfig({
poolKey: PoolKey({
currency0: Currency.wrap(nimpetTokenAddr),
currency0: Currency.wrap(sprotoTokenAddr),
currency1: Currency.wrap(usdc),
fee: 10000,
tickSpacing: 0,
Expand All @@ -156,24 +156,24 @@ contract AddGloriaAndOtherDistributions is Script {
);

vm.stopBroadcast();
console.log("Nimpet => USDC Pool config proposed, ID: %s", configId);
console.log("Sproto => USDC Pool config proposed, ID: %s", configId);

return configId;
}

function deployNimpetDistribution() public returns (uint256) {
function deploySprotoDistribution() public returns (uint256) {
TokenDistributor distributor = TokenDistributor(payable(distributorAddr));

Action[] memory actions1 = new DistributionBuilder()
.buy(2_000, aitvTokenAddr, address(0))
.buy(8_000, nimpetTokenAddr, address(0))
.buy(8_000, sprotoTokenAddr, address(0))
.build();

vm.startBroadcast();
uint256 distributionId = distributor.addDistribution(actions1);
vm.stopBroadcast();

console.log("TokenDistributor (Nimpet) distribution added, ID: %s", distributionId);
console.log("TokenDistributor (Sproto) distribution added, ID: %s", distributionId);

return distributionId;
}
Expand Down
6 changes: 5 additions & 1 deletion script/DeployTokenDistributor.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {IUniversalRouter} from "@uniswap/universal-router/src/interfaces/IUniver
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {IAerodromeRouter} from "../src/interfaces/IAerodromeRouter.sol";
import {IPancakeSmartRouter} from "../src/interfaces/IPancakeSmartRouter.sol";
import {TokenDistributor} from "../src/TokenDistributor.sol";

contract DeployTokenDistributorScript is Script {
Expand All @@ -15,14 +16,16 @@ contract DeployTokenDistributorScript is Script {
address permit2 = vm.envAddress("PERMIT2");
address weth = vm.envAddress("WETH");
address aerodromeRouter = vm.envAddress("AERODROME_ROUTER");
address pancakeSmartRouter = vm.envAddress("PANCAKE_SMART_ROUTER");

vm.startBroadcast();
TokenDistributor distributor = new TokenDistributor(
owner,
IUniversalRouter(uniswapUniversalRouter),
IPermit2(permit2),
weth,
IAerodromeRouter(aerodromeRouter)
IAerodromeRouter(aerodromeRouter),
IPancakeSmartRouter(pancakeSmartRouter)
);
vm.stopBroadcast();

Expand All @@ -33,5 +36,6 @@ contract DeployTokenDistributorScript is Script {
require(permit2 == address(distributor.permit2()), "Permit2 mismatch");
require(weth == address(distributor.weth()), "WETH mismatch");
require(aerodromeRouter == address(distributor.aerodromeRouter()), "AerodromeRouter mismatch");
require(pancakeSmartRouter == address(distributor.pancakeSmartRouter()), "PancakeSmartRouter mismatch");
}
}
85 changes: 80 additions & 5 deletions src/TokenDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {PoolConfig} from "./types/PoolConfig.sol";
import {UniswapVersion} from "./types/UniswapVersion.sol";
import {AerodromeConfig, AerodromeProposal} from "./types/AerodromeConfig.sol";
import {IAerodromeRouter} from "./interfaces/IAerodromeRouter.sol";
import {IPancakeSmartRouter} from "./interfaces/IPancakeSmartRouter.sol";
import {PancakeConfig, PancakeProposal} from "./types/PancakeConfig.sol";
import {PancakeSwapper} from "./libraries/PancakeSwapper.sol";

enum ActionType {
Burn,
Expand Down Expand Up @@ -130,6 +133,18 @@ contract TokenDistributor is Ownable2Step, ReentrancyGuard {
address indexed tokenB,
bool stable
);
event PancakeConfigProposed(
uint256 proposalId,
bytes32 key,
address indexed tokenA,
address indexed tokenB
);
event PancakeConfigSet(
uint256 proposalId,
bytes32 key,
address indexed tokenA,
address indexed tokenB
);
event DistributionAdded(uint256 indexed distributionId, address indexed sender);
event DistributionIdSet(bytes32 indexed distributionName, uint256 indexed distributionId);
event Distribution(
Expand All @@ -144,13 +159,16 @@ contract TokenDistributor is Ownable2Step, ReentrancyGuard {
IUniversalRouter public uniswapUniversalRouter;
IPermit2 public permit2;
IAerodromeRouter public aerodromeRouter;
IPancakeSmartRouter public pancakeSmartRouter;
address public weth;

mapping(bytes32 distributionName => uint256 distributionId) internal distributionNameToId;
PoolConfig[] internal poolProposals;
mapping(bytes32 key => PoolConfig config) internal pools;
AerodromeProposal[] internal aerodromeProposals;
mapping(bytes32 key => AerodromeConfig config) internal aerodromePools;
PancakeProposal[] internal pancakeProposals;
mapping(bytes32 key => PancakeConfig config) internal pancakePools;
mapping(uint256 distributionId => bytes distribution) internal distributions;
uint256 internal lastDistributionId;

Expand All @@ -164,16 +182,18 @@ contract TokenDistributor is Ownable2Step, ReentrancyGuard {
IUniversalRouter _uniswapUniversalRouter,
IPermit2 _permit2,
address _weth,
IAerodromeRouter _aerodromeRouter
IAerodromeRouter _aerodromeRouter,
IPancakeSmartRouter _pancakeSmartRouter
) Ownable(_owner) {
if (address(_uniswapUniversalRouter) == address(0) || address(_permit2) == address(0) || _weth == address(0) || address(_aerodromeRouter) == address(0)) {
if (address(_uniswapUniversalRouter) == address(0) || address(_permit2) == address(0) || _weth == address(0) || address(_aerodromeRouter) == address(0) || address(_pancakeSmartRouter) == address(0)) {
revert ZeroAddressNotAllowed();
}

uniswapUniversalRouter = _uniswapUniversalRouter;
permit2 = _permit2;
weth = _weth;
aerodromeRouter = _aerodromeRouter;
pancakeSmartRouter = _pancakeSmartRouter;
}

/// @notice Adds a distribution to the contract
Expand Down Expand Up @@ -310,6 +330,34 @@ contract TokenDistributor is Ownable2Step, ReentrancyGuard {
return aerodromePools[_getSwapPairKey(_tokenA, _tokenB)];
}

/// @notice Proposes a PancakeSwap route config
/// @param _proposal The proposed Pancake config
/// @return id The id of the proposal
function proposePancakeConfig(PancakeProposal calldata _proposal) external returns (uint256) {
if (_proposal.tokenA == address(0) || _proposal.tokenB == address(0)) {
revert ZeroAddressNotAllowed();
}
pancakeProposals.push(_proposal);
uint256 id = pancakeProposals.length - 1;
bytes32 key = _getSwapPairKey(_proposal.tokenA, _proposal.tokenB);
emit PancakeConfigProposed(id, key, _proposal.tokenA, _proposal.tokenB);
return id;
}

/// @notice Sets the PancakeSwap route config
/// @param _proposalId The id of the Pancake config proposal
function setPancakeConfig(uint256 _proposalId) external onlyOwner {
PancakeProposal memory p = pancakeProposals[_proposalId];
bytes32 key = _getSwapPairKey(p.tokenA, p.tokenB);
pancakePools[key] = PancakeConfig({fee: p.fee, exists: true});
emit PancakeConfigSet(_proposalId, key, p.tokenA, p.tokenB);
}

/// @notice Gets the Pancake config for a given token pair
function getPancakeConfig(address _tokenA, address _tokenB) external view returns (PancakeConfig memory) {
return pancakePools[_getSwapPairKey(_tokenA, _tokenB)];
}

/// @notice Gets the pool config for a given pool
/// @param _tokenA The address of the first token
/// @param _tokenB The address of the second token
Expand Down Expand Up @@ -646,10 +694,15 @@ contract TokenDistributor is Ownable2Step, ReentrancyGuard {
outAmount = _uniExactIn(poolConfig, _paymentToken, _action.token, _actionTotalAmount, minOut, _ctx.deadline);
} else {
AerodromeConfig memory aero = aerodromePools[pairKey];
if (!aero.exists) {
revert PoolConfigNotFound(_paymentToken, _action.token);
if (aero.exists) {
outAmount = _aeroExactIn(aero, _paymentToken, _action.token, _actionTotalAmount, minOut, _ctx.deadline);
} else {
PancakeConfig memory cake = pancakePools[pairKey];
if (!cake.exists) {
revert PoolConfigNotFound(_paymentToken, _action.token);
}
outAmount = _pancakeExactIn(_paymentToken, _action.token, _actionTotalAmount, minOut, _ctx.deadline);
}
outAmount = _aeroExactIn(aero, _paymentToken, _action.token, _actionTotalAmount, minOut, _ctx.deadline);
}
++_ctx.minAmountsOutIndex;
}
Expand Down Expand Up @@ -699,6 +752,28 @@ contract TokenDistributor is Ownable2Step, ReentrancyGuard {
);
}

function _pancakeExactIn(
address _tokenIn,
address _tokenOut,
uint256 _amountIn,
uint128 _minOut,
uint256 _deadline
) internal returns (uint256) {
bytes32 pairKey = _getSwapPairKey(_tokenIn, _tokenOut);
PancakeConfig memory cake = pancakePools[pairKey];
return PancakeSwapper.swapExactIn(
address(this),
pancakeSmartRouter,
_tokenIn,
_tokenOut,
_amountIn,
_minOut,
_deadline,
cake,
weth
);
}

function _poolConfigExists(PoolConfig memory _pc) internal pure returns (bool) {
return !(Currency.unwrap(_pc.poolKey.currency0) == address(0) && Currency.unwrap(_pc.poolKey.currency1) == address(0));
}
Expand Down
42 changes: 42 additions & 0 deletions src/interfaces/IPancakeSmartRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library IV3SwapRouter {
struct ExactInputParams {
bytes path;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
}

struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
}

interface IPancakeSmartRouter {
function exactInput(IV3SwapRouter.ExactInputParams calldata params) external payable returns (uint256 amountOut);

function exactInputSingle(IV3SwapRouter.ExactInputSingleParams calldata params)
external
payable
returns (uint256 amountOut);

function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to
) external payable returns (uint256 amountOut);

function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory);

function multicall(bytes[] calldata data) external payable returns (bytes[] memory);
}

81 changes: 81 additions & 0 deletions src/libraries/PancakeSwapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPancakeSmartRouter, IV3SwapRouter} from "../interfaces/IPancakeSmartRouter.sol";
import {PancakeConfig} from "../types/PancakeConfig.sol";

library PancakeSwapper {
using SafeERC20 for IERC20;

/// @notice Do an exact-in swap via Pancake Smart Router (v2-style or v3 single)
/// @param recipient Who receives tokenOut
/// @param router Pancake Smart Router instance
/// @param tokenIn The ERC20 token being sold
/// @param tokenOut The ERC20 token being bought
/// @param amountIn How much tokenIn to sell
/// @param amountOutMin Minimum acceptable tokenOut
/// @param deadline Deadline forwarded to multicall
/// @param config Pancake config (fee=0 uses v2-style path; >0 uses v3 exactInputSingle)
/// @param weth WETH address (used as hop for v2-style path when neither side is WETH)
/// @return amountOut Amount of tokenOut received by recipient
function swapExactIn(
address recipient,
IPancakeSmartRouter router,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint128 amountOutMin,
uint256 deadline,
PancakeConfig memory config,
address weth
) internal returns (uint256 amountOut) {
uint256 startBal = IERC20(tokenOut).balanceOf(recipient);

if (IERC20(tokenIn).allowance(address(this), address(router)) < amountIn) {
IERC20(tokenIn).forceApprove(address(router), type(uint256).max);
}

bytes[] memory calls = new bytes[](1);

if (config.fee != 0) {
IV3SwapRouter.ExactInputSingleParams memory params = IV3SwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: config.fee,
recipient: recipient,
amountIn: amountIn,
amountOutMinimum: amountOutMin,
sqrtPriceLimitX96: 0
});
calls[0] = abi.encodeWithSelector(IPancakeSmartRouter.exactInputSingle.selector, params);
} else {
address[] memory path;
if (tokenIn == weth || tokenOut == weth) {
path = new address[](2);
path[0] = tokenIn;
path[1] = tokenOut;
} else {
path = new address[](3);
path[0] = tokenIn;
path[1] = weth;
path[2] = tokenOut;
}
calls[0] = abi.encodeWithSelector(
IPancakeSmartRouter.swapExactTokensForTokens.selector,
amountIn,
amountOutMin,
path,
recipient
);
}

router.multicall(deadline, calls);

uint256 endBal = IERC20(tokenOut).balanceOf(recipient);
return endBal - startBal;
}
}


Loading