Skip to content

Commit

Permalink
Merge pull request #296 from 1inch/fix/chainlink_calculator
Browse files Browse the repository at this point in the history
[SC-994] fix ChainlinkCalculator getters
  • Loading branch information
ZumZoom authored Dec 13, 2023
2 parents c6d131e + df0b0d5 commit 827164d
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 43 deletions.
79 changes: 45 additions & 34 deletions contracts/extensions/ChainlinkCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ contract ChainlinkCalculator is IAmountGetter {

uint256 private constant _SPREAD_DENOMINATOR = 1e9;
uint256 private constant _ORACLE_TTL = 4 hours;
bytes1 private constant _INVERSE_FLAG = 0x80;
bytes1 private constant _DOUBLE_PRICE_FLAG = 0x40;

/// @notice Calculates price of token A relative to token B. Note that order is important
/// @return result Token A relative price times amount
function doublePrice(AggregatorV3Interface oracle1, AggregatorV3Interface oracle2, int256 decimalsScale, uint256 amount) external view returns(uint256 result) {
return _doublePrice(oracle1, oracle2, decimalsScale, amount);
}

function getMakingAmount(
IOrderMixin.Order calldata /* order */,
Expand All @@ -28,20 +36,7 @@ contract ChainlinkCalculator is IAmountGetter {
uint256 /* remainingMakingAmount */,
bytes calldata extraData
) external view returns (uint256) {
(
AggregatorV3Interface oracle,
uint256 spread
) = abi.decode(extraData, (AggregatorV3Interface, uint256));

/// @notice Calculates price of token relative to oracle unit (ETH or USD)
/// Lowest 254 bits specify spread amount. Spread is scaled by 1e9, i.e. 101% = 1.01e9, 99% = 0.99e9.
/// Highest bit is set when oracle price should be inverted,
/// e.g. for DAI-ETH oracle, inverse=false means that we request DAI price in ETH
/// and inverse=true means that we request ETH price in DAI
/// @return Amount * spread * oracle price
(, int256 latestAnswer,, uint256 updatedAt,) = oracle.latestRoundData();
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
return takingAmount * spread * latestAnswer.toUint256() / (10 ** oracle.decimals()) / _SPREAD_DENOMINATOR;
return _getSpreadedAmount(takingAmount, extraData);
}

function getTakingAmount(
Expand All @@ -53,31 +48,46 @@ contract ChainlinkCalculator is IAmountGetter {
uint256 /* remainingMakingAmount */,
bytes calldata extraData
) external view returns (uint256) {
(
AggregatorV3Interface oracle,
uint256 spread
) = abi.decode(extraData, (AggregatorV3Interface, uint256));
return _getSpreadedAmount(makingAmount, extraData);
}

/// @notice Calculates price of token relative to oracle unit (ETH or USD)
/// Lowest 254 bits specify spread amount. Spread is scaled by 1e9, i.e. 101% = 1.01e9, 99% = 0.99e9.
/// Highest bit is set when oracle price should be inverted,
/// e.g. for DAI-ETH oracle, inverse=false means that we request DAI price in ETH
/// and inverse=true means that we request ETH price in DAI
/// @return Amount * spread * oracle price
(, int256 latestAnswer,, uint256 updatedAt,) = oracle.latestRoundData();
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
return makingAmount * spread * (10 ** oracle.decimals()) / latestAnswer.toUint256() / _SPREAD_DENOMINATOR;
/// @notice Calculates price of token relative to oracle unit (ETH or USD)
/// The first byte of the blob contain inverse and useDoublePrice flags,
/// The inverse flag is set when oracle price should be inverted,
/// e.g. for DAI-ETH oracle, inverse=false means that we request DAI price in ETH
/// and inverse=true means that we request ETH price in DAI
/// The useDoublePrice flag is set when needs price for two custom tokens (other than ETH or USD)
/// @return Amount * spread * oracle price
function _getSpreadedAmount(uint256 amount, bytes calldata blob) internal view returns(uint256) {
bytes1 flags = bytes1(blob[:1]);
if (flags & _DOUBLE_PRICE_FLAG == _DOUBLE_PRICE_FLAG) {
AggregatorV3Interface oracle1 = AggregatorV3Interface(address(bytes20(blob[1:21])));
AggregatorV3Interface oracle2 = AggregatorV3Interface(address(bytes20(blob[21:41])));
int256 decimalsScale = int256(uint256(bytes32(blob[41:73])));
uint256 spread = uint256(bytes32(blob[73:105]));
return _doublePrice(oracle1, oracle2, decimalsScale, spread * amount) / _SPREAD_DENOMINATOR;
} else {
AggregatorV3Interface oracle = AggregatorV3Interface(address(bytes20(blob[1:21])));
uint256 spread = uint256(bytes32(blob[21:53]));
(, int256 latestAnswer,, uint256 updatedAt,) = oracle.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
if (flags & _INVERSE_FLAG == _INVERSE_FLAG) {
return spread * amount * (10 ** oracle.decimals()) / latestAnswer.toUint256() / _SPREAD_DENOMINATOR;
} else {
return spread * amount * latestAnswer.toUint256() / (10 ** oracle.decimals()) / _SPREAD_DENOMINATOR;
}
}
}

/// @notice Calculates price of token A relative to token B. Note that order is important
/// @return result Token A relative price times amount
function doublePrice(AggregatorV3Interface oracle1, AggregatorV3Interface oracle2, uint256 spread, int256 decimalsScale, uint256 amount) external view returns(uint256 result) {
function _doublePrice(AggregatorV3Interface oracle1, AggregatorV3Interface oracle2, int256 decimalsScale, uint256 amount) internal view returns(uint256 result) {
if (oracle1.decimals() != oracle2.decimals()) revert DifferentOracleDecimals();

{
(, int256 latestAnswer1,, uint256 updatedAt,) = oracle1.latestRoundData();
(, int256 latestAnswer,, uint256 updatedAt,) = oracle1.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
result = amount * spread * latestAnswer1.toUint256();
result = amount * latestAnswer.toUint256();
}

if (decimalsScale > 0) {
Expand All @@ -87,9 +97,10 @@ contract ChainlinkCalculator is IAmountGetter {
}

{
(, int256 latestAnswer2,, uint256 updatedAt,) = oracle2.latestRoundData();
(, int256 latestAnswer,, uint256 updatedAt,) = oracle2.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (updatedAt + _ORACLE_TTL < block.timestamp) revert StaleOraclePrice();
result /= latestAnswer2.toUint256() * _SPREAD_DENOMINATOR;
result /= latestAnswer.toUint256();
}
}
}
Expand Down
152 changes: 143 additions & 9 deletions test/ChainLinkExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ const { signOrder, buildOrder, buildTakerTraits } = require('./helpers/orderUtil
const { deploySwapTokens } = require('./helpers/fixtures');
const { ethers } = require('hardhat');

function buildSinglePriceCalldata ({ chainlinkCalcAddress, oracleAddress, spread, inverse = false }) {
return ethers.utils.solidityPack(
['address', 'bytes1', 'address', 'uint256'],
[chainlinkCalcAddress, inverse ? 0x80 : 0, oracleAddress, spread],
);
}

function buildDoublePriceCalldata ({ chainlinkCalcAddress, oracleAddress1, oracleAddress2, decimalsScale, spread }) {
return ethers.utils.solidityPack(
['address', 'bytes1', 'address', 'address', 'int256', 'uint256'],
[chainlinkCalcAddress, 0x40, oracleAddress1, oracleAddress2, decimalsScale, spread],
);
}

describe('ChainLinkExample', function () {
let addr, addr1;

Expand Down Expand Up @@ -45,6 +59,8 @@ describe('ChainLinkExample', function () {
it('eth -> dai chainlink+eps order', async function () {
const { dai, weth, swap, chainId, chainlink, daiOracle } = await loadFixture(deployContractsAndInit);

const chainlinkCalcAddress = chainlink.address;
const oracleAddress = daiOracle.address;
// chainlink rate is 1 eth = 4000 dai
const order = buildOrder(
{
Expand All @@ -55,23 +71,141 @@ describe('ChainLinkExample', function () {
maker: addr1.address,
},
{
makingAmountData: ethers.utils.solidityPack(['address', 'uint256', 'uint256'], [chainlink.address, daiOracle.address, '990000000']), // maker offset is 0.99
takingAmountData: ethers.utils.solidityPack(['address', 'uint256', 'uint256'], [chainlink.address, daiOracle.address, '1010000000']), // taker offset is 1.01
makingAmountData: buildSinglePriceCalldata({ chainlinkCalcAddress, oracleAddress, spread: '990000000' }), // maker offset is 0.99
takingAmountData: buildSinglePriceCalldata({ chainlinkCalcAddress, oracleAddress, spread: '1010000000', inverse: true }), // taker offset is 1.01
},
);

const signature = await signOrder(order, chainId, swap.address, addr1);

const { r, _vs: vs } = ethers.utils.splitSignature(signature);
// taking threshold = 4000 + 1% + eps
const takerTraits = buildTakerTraits({
extension: order.extension,
threshold: ether('0.99'),
});
const fillTx = swap.fillOrderArgs(order, r, vs, ether('4000'), takerTraits.traits, takerTraits.args);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [ether('-4000'), ether('4000')]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1], [ether('0.99'), ether('-0.99')]);
});

it('dai -> eth chainlink+eps order', async function () {
const { dai, weth, swap, chainId, chainlink, daiOracle } = await loadFixture(deployContractsAndInit);

const chainlinkCalcAddress = chainlink.address;
const oracleAddress = daiOracle.address;
// chainlink rate is 1 eth = 4000 dai
const order = buildOrder(
{
makerAsset: dai.address,
takerAsset: weth.address,
makingAmount: ether('4000').toString(),
takingAmount: ether('1').toString(),
maker: addr1.address,
},
{
makingAmountData: buildSinglePriceCalldata({ chainlinkCalcAddress, oracleAddress, spread: '990000000', inverse: true }), // maker offset is 0.99
takingAmountData: buildSinglePriceCalldata({ chainlinkCalcAddress, oracleAddress, spread: '1010000000' }), // taker offset is 1.01
},
);
const signature = await signOrder(order, chainId, swap.address, addr1);

const { r, _vs: vs } = ethers.utils.splitSignature(signature);
// taking threshold = 1 eth + 1% + eps
const takerTraits = buildTakerTraits({
makingAmount: true,
extension: order.extension,
threshold: ether('4040.01'),
threshold: ether('1.01'),
});
const fillTx = swap.fillOrderArgs(order, r, vs, ether('4000'), takerTraits.traits, takerTraits.args);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [ether('4000'), ether('-4000')]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1], [ether('-1.01'), ether('1.01')]);
});

it('dai -> 1inch chainlink+eps order (check takingAmountData)', async function () {
const { dai, inch, swap, chainId, chainlink, daiOracle, inchOracle } = await loadFixture(deployContractsAndInit);

const chainlinkCalcAddress = chainlink.address;
const oracleAddress1 = inchOracle.address;
const oracleAddress2 = daiOracle.address;
const makingAmount = ether('100');
const takingAmount = ether('632');
const decimalsScale = '0';
const takingSpread = 1010000000n; // taker offset is 1.01
const order = buildOrder(
{
makerAsset: inch.address,
takerAsset: dai.address,
makingAmount,
takingAmount,
maker: addr1.address,
},
{
makingAmountData: buildDoublePriceCalldata(
{ chainlinkCalcAddress, oracleAddress1: oracleAddress2, oracleAddress2: oracleAddress1, decimalsScale, spread: '990000000' }, // maker offset is 0.99
),
takingAmountData: buildDoublePriceCalldata(
{ chainlinkCalcAddress, oracleAddress1, oracleAddress2, decimalsScale, spread: takingSpread.toString() },
),
},
);
const signature = await signOrder(order, chainId, swap.address, addr1);

const { r, _vs: vs } = ethers.utils.splitSignature(signature);
// taking threshold = 1 eth + 1% + eps
const takerTraits = buildTakerTraits({
makingAmount: true,
extension: order.extension,
threshold: takingAmount.toBigInt() * takingSpread / 1000000000n + ether('0.01').toBigInt(),
});
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
const realTakingAmount = makingAmount.toBigInt() * // <-- makingAmount * spread / 1e9 * inchPrice / daiPrice
takingSpread / 1000000000n *
BigInt((await inchOracle.latestRoundData())[1]) / BigInt((await daiOracle.latestRoundData())[1]);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [-realTakingAmount, realTakingAmount]);
await expect(fillTx).to.changeTokenBalances(inch, [addr, addr1], [makingAmount, makingAmount.mul(-1)]);
});

it('dai -> 1inch chainlink+eps order (check makingAmountData)', async function () {
const { dai, inch, swap, chainId, chainlink, daiOracle, inchOracle } = await loadFixture(deployContractsAndInit);

const chainlinkCalcAddress = chainlink.address;
const oracleAddress1 = inchOracle.address;
const oracleAddress2 = daiOracle.address;
const makingAmount = ether('100').toBigInt();
const takingAmount = ether('632').toBigInt();
const decimalsScale = 0n;
const makingSpread = 990000000n; // maker offset is 0.99
const order = buildOrder(
{
makerAsset: inch.address,
takerAsset: dai.address,
makingAmount,
takingAmount,
maker: addr1.address,
},
{
makingAmountData: buildDoublePriceCalldata(
{ chainlinkCalcAddress, oracleAddress1: oracleAddress2, oracleAddress2: oracleAddress1, decimalsScale, spread: makingSpread },
),
takingAmountData: buildDoublePriceCalldata(
{ chainlinkCalcAddress, oracleAddress1, oracleAddress2, decimalsScale, spread: 1010000000n }, // taker offset is 1.01
),
},
);
const signature = await signOrder(order, chainId, swap.address, addr1);

const { r, _vs: vs } = ethers.utils.splitSignature(signature);
// taking threshold = 1 eth + 1% + eps
const takerTraits = buildTakerTraits({
extension: order.extension,
threshold: makingAmount * makingSpread / 1000000000n + ether('0.01').toBigInt(),
});
const fillTx = swap.fillOrderArgs(order, r, vs, ether('1'), takerTraits.traits, takerTraits.args);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [ether('-4040'), ether('4040')]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1], [ether('1'), ether('-1')]);
const fillTx = swap.fillOrderArgs(order, r, vs, takingAmount, takerTraits.traits, takerTraits.args);
const realMakingAmount = takingAmount * // <-- takingAmount * spread / 1e9 * daiPrice / inchPrice
makingSpread / 1000000000n *
BigInt((await daiOracle.latestRoundData())[1]) / BigInt((await inchOracle.latestRoundData())[1]);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [-takingAmount, takingAmount]);
await expect(fillTx).to.changeTokenBalances(inch, [addr, addr1], [realMakingAmount, -realMakingAmount]);
});

it('dai -> 1inch stop loss order', async function () {
Expand All @@ -81,7 +215,7 @@ describe('ChainLinkExample', function () {
const takingAmount = ether('631');
const priceCall = swap.interface.encodeFunctionData('arbitraryStaticCall', [
chainlink.address,
chainlink.interface.encodeFunctionData('doublePrice', [inchOracle.address, daiOracle.address, '1000000000', '0', ether('1')]),
chainlink.interface.encodeFunctionData('doublePrice', [inchOracle.address, daiOracle.address, '0', ether('1')]),
]);

const order = buildOrder(
Expand Down Expand Up @@ -114,7 +248,7 @@ describe('ChainLinkExample', function () {

const makingAmount = ether('100');
const takingAmount = ether('631');
const priceCall = chainlink.interface.encodeFunctionData('doublePrice', [inchOracle.address, daiOracle.address, '1000000000', '0', ether('1')]);
const priceCall = chainlink.interface.encodeFunctionData('doublePrice', [inchOracle.address, daiOracle.address, '0', ether('1')]);

const order = buildOrder(
{
Expand Down

0 comments on commit 827164d

Please sign in to comment.