diff --git a/addresses/euler-addresses-goerli.json b/addresses/euler-addresses-goerli.json index fb42ad9f..64b3be7e 100644 --- a/addresses/euler-addresses-goerli.json +++ b/addresses/euler-addresses-goerli.json @@ -23,10 +23,12 @@ "CRV": "0x87351b560ebc1810CF33cBA7A0b508e2Cf36e821" }, "eulerGeneralView": "0x486492546998494C224b294aE8c5a2982946220B", + "eulerSimpleLens": "0x62626a0f051B547b3182e55D1Eba667138790D8D", "euler": "0x931172BB95549d0f29e10ae2D079ABA3C63318B3", "installer": "0xB21fb96025c2b7F618ba9D3ae955D20Fa303b2D2", "markets": "0x3EbC39b84B1F856fAFE9803A9e1Eae7Da016Da36", "liquidation": "0x66326c072283feE63E1C3feF9BD024F8697EC1BB", "governance": "0x496A8344497875D0D805167874f2f938aEa15600", - "exec": "0x4b62EB6797526491eEf6eF36D3B9960E5d66C394" + "exec": "0x4b62EB6797526491eEf6eF36D3B9960E5d66C394", + "testERC20TokenFaucet": "0x1215396CB53774dCE60978d7237F32042cF3a1db" } diff --git a/contracts/test/MockEACAggregatorProxy.sol b/contracts/test/MockEACAggregatorProxy.sol index c75effd7..fb7100b8 100644 --- a/contracts/test/MockEACAggregatorProxy.sol +++ b/contracts/test/MockEACAggregatorProxy.sol @@ -10,6 +10,7 @@ contract MockAggregatorProxy { uint80 answeredInRound; // The round ID of the round in which the answer was computed. } + address public owner; string constant description_ = "Mock Aggregator"; uint256 constant version_ = 1; uint8 public decimals_; @@ -18,14 +19,24 @@ contract MockAggregatorProxy { constructor(uint8 _decimals) { decimals_ = _decimals; + owner = msg.sender; } - function mockSetData(Data calldata data) external { + modifier onlyOwner() { + require(msg.sender == owner, "unauthorized"); + _; + } + + function changeOwner(address newOwner) external onlyOwner { + owner = newOwner; + } + + function mockSetData(Data calldata data) external onlyOwner { data_[data.roundId] = data; currentRoundId_ = data.roundId; } - function mockSetValidAnswer(int256 answer) external { + function mockSetValidAnswer(int256 answer) external onlyOwner { currentRoundId_++; data_[currentRoundId_] = Data( diff --git a/contracts/test/TestERC20TokenFaucet.sol b/contracts/test/TestERC20TokenFaucet.sol new file mode 100644 index 00000000..b638392b --- /dev/null +++ b/contracts/test/TestERC20TokenFaucet.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +interface IERC20 { + function transfer(address recipient, uint256 amount) external returns (bool); + function decimals() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); +} + +contract TestERC20TokenFaucet { + address public owner; + mapping(address => uint256) thresholds; + + constructor() { + owner = msg.sender; + } + + function withdraw(address underlying) external { + uint256 balance = IERC20(underlying).balanceOf(msg.sender); + uint256 amount = getThreshold(underlying); + require(balance < amount, "withdraw: balance not below threshold"); + IERC20(underlying).transfer(msg.sender, amount - balance); + } + + function getThreshold(address underlying) public view returns(uint256) { + return thresholds[underlying]; + } + + // Owner functions + + modifier onlyOwner { + require(msg.sender == owner, "unauthorized"); + _; + } + + function setThreshold(address underlying, uint256 _threshold) external onlyOwner { + require(_threshold > 0, "setThreshold: threshold must be greater than zero"); + thresholds[underlying] = _threshold; + } + + function reduceFaucetBalance(address underlying, uint256 amount) external onlyOwner { + if (amount == type(uint256).max) { + uint balance = IERC20(underlying).balanceOf(address(this)); + IERC20(underlying).transfer(msg.sender, balance); + } else { + IERC20(underlying).transfer(msg.sender, amount); + } + } +} diff --git a/test/chainlinkPriceFeed.js b/test/chainlinkPriceFeed.js index 46bbada4..8b1ec9d6 100644 --- a/test/chainlinkPriceFeed.js +++ b/test/chainlinkPriceFeed.js @@ -14,6 +14,71 @@ et.testSet({ ] }) +.test({ + desc: "set the correct owner upon deployment", + actions: ctx => [ + { call: 'AggregatorTST.owner', args: [], assertEql: ctx.wallet.address, }, + ] +}) + +.test({ + desc: "reverts if non-owner attempts to set price via mockSetValidAnswer", + actions: ctx => [ + { call: 'AggregatorTST.latestAnswer', args: [], assertEql: 0, }, + + { from: ctx.wallet2, send: 'AggregatorTST.mockSetValidAnswer', args: [et.eth('1')], + expectError: 'unauthorized', + }, + + { call: 'AggregatorTST.latestAnswer', args: [], assertEql: 0, }, + + { from: ctx.wallet, send: 'AggregatorTST.mockSetValidAnswer', args: [et.eth('1')], }, + + { call: 'AggregatorTST.latestAnswer', args: [], assertEql: et.eth('1'), }, + ] +}) + +.test({ + desc: "reverts if non-owner attempts to set price via mockSetData", + actions: ctx => [ + { action: 'cb', cb: async () => { + let latestAnswer = await ctx.contracts.AggregatorTST.latestAnswer(); + et.expect(latestAnswer).to.equal(0); + + let errMsg = ''; + try { + await ctx.contracts.AggregatorTST.connect(ctx.wallet2).mockSetData([1, 123456, 0, 0, 0]); + latestAnswer = await ctx.contracts.AggregatorTST.latestAnswer(); + et.expect(latestAnswer).to.equal(0); + + await ctx.contracts.AggregatorTST.connect(ctx.wallet).mockSetData([1, 123456, 0, 0, 0]); + latestAnswer = await ctx.contracts.AggregatorTST.latestAnswer(); + et.expect(latestAnswer).to.equal(123456); + } catch (e) { + errMsg = e.message; + } + et.expect(errMsg).to.contains('unauthorized'); + }}, + ] +}) + +.test({ + desc: "reverts if non-owner attempts to change owner", + actions: ctx => [ + { call: 'AggregatorTST.owner', args: [], assertEql: ctx.wallet.address, }, + + { from: ctx.wallet2, send: 'AggregatorTST.changeOwner', args: [ctx.wallet2.address], + expectError: 'unauthorized', + }, + + { call: 'AggregatorTST.owner', args: [], assertEql: ctx.wallet.address, }, + + { from: ctx.wallet, send: 'AggregatorTST.changeOwner', args: [ctx.wallet2.address], }, + + { call: 'AggregatorTST.owner', args: [], assertEql: ctx.wallet2.address, }, + ] +}) + .test({ desc: "chainlink pricing setup and price fetch", actions: ctx => [ diff --git a/test/lib/eTestLib.js b/test/lib/eTestLib.js index 4c90ae81..4aec8b3c 100644 --- a/test/lib/eTestLib.js +++ b/test/lib/eTestLib.js @@ -88,6 +88,7 @@ const contractNames = [ // Testing 'TestERC20', + 'TestERC20TokenFaucet', 'MockUniswapV3Factory', 'EulerGeneralView', 'InvariantChecker', diff --git a/test/tokenFaucet.js b/test/tokenFaucet.js new file mode 100644 index 00000000..4bdd30d0 --- /dev/null +++ b/test/tokenFaucet.js @@ -0,0 +1,194 @@ +const et = require('./lib/eTestLib'); + +et.testSet({ + desc: "ERC20 test token faucet", + preActions: ctx => [ + { action: 'cb', cb: async () => { + // deploy faucet + + ctx.contracts.TestERC20Faucet = await (await ctx.factories.TestERC20TokenFaucet.deploy()).deployed(); + }} + ] +}) + + +.test({ + desc: "only owner can set threshold", + actions: ctx => [ + { from: ctx.wallet2, send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, 2], + expectError: 'unauthorized', + }, + + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, 2], }, + + { call: 'TestERC20Faucet.getThreshold', args: [ctx.contracts.tokens.TST.address], assertEql: 2, }, + ], +}) + + +.test({ + desc: "owner can set threshold for multiple tokens", + actions: ctx => [ + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, 2], }, + + { call: 'TestERC20Faucet.getThreshold', args: [ctx.contracts.tokens.TST.address], assertEql: 2, }, + + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST2.address, 12], }, + + { call: 'TestERC20Faucet.getThreshold', args: [ctx.contracts.tokens.TST2.address], assertEql: 12, }, + ], +}) + + +.test({ + desc: "zero threshold reverts", + actions: ctx => [ + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, 0], + expectError: 'setThreshold: threshold must be greater than zero', + }, + ], +}) + + +.test({ + desc: "only owner can reduce faucet balance", + actions: ctx => [ + { send: 'tokens.TST.mint', args: [ctx.contracts.TestERC20Faucet.address, 100], }, + + { call: 'tokens.TST.balanceOf', args: [ctx.contracts.TestERC20Faucet.address], assertEql: 100, }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet.address], assertEql: 0, }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.reduceFaucetBalance', args: [ctx.contracts.tokens.TST.address, 2], + expectError: 'unauthorized', + }, + + { send: 'TestERC20Faucet.reduceFaucetBalance', args: [ctx.contracts.tokens.TST.address, 2], }, + + { call: 'tokens.TST.balanceOf', args: [ctx.contracts.TestERC20Faucet.address], assertEql: 98, }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet.address], assertEql: 2, }, + ], +}) + + +.test({ + desc: "owner can reduce faucet balance to zero using max uint256", + actions: ctx => [ + { send: 'tokens.TST.mint', args: [ctx.contracts.TestERC20Faucet.address, 100], }, + + { call: 'tokens.TST.balanceOf', args: [ctx.contracts.TestERC20Faucet.address], assertEql: 100, }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet.address], assertEql: 0, }, + + { send: 'TestERC20Faucet.reduceFaucetBalance', args: [ctx.contracts.tokens.TST.address, et.MaxUint256], }, + + { call: 'tokens.TST.balanceOf', args: [ctx.contracts.TestERC20Faucet.address], assertEql: 0, }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet.address], assertEql: 100, }, + ], +}) + + +.test({ + desc: "withdraw only tops up user balance and does not issue full threshold", + actions: ctx => [ + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, et.eth(1)], }, + + { call: 'TestERC20Faucet.getThreshold', args: [ctx.contracts.tokens.TST.address], assertEql: et.eth(1), }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: 0, }, + + { send: 'tokens.TST.mint', args: [ctx.contracts.TestERC20Faucet.address, et.eth(50)], }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.withdraw', args: [ctx.contracts.tokens.TST.address, ], }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: et.eth(1), }, + + { from: ctx.wallet2, send: 'tokens.TST.transfer', args: [ctx.wallet3.address, et.eth(0.5)], }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet3.address], assertEql: et.eth(0.5), }, + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: et.eth(0.5), }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.withdraw', args: [ctx.contracts.tokens.TST.address, ], }, + { call: 'tokens.TST.balanceOf', args: [ctx.wallet3.address], assertEql: et.eth(0.5), }, + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: et.eth(1), }, + ], +}) + + +.test({ + desc: "withdraw reverts if threshold is not set", + actions: ctx => [ + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: 0, }, + + { send: 'tokens.TST.mint', args: [ctx.contracts.TestERC20Faucet.address, et.eth(50)], }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.withdraw', args: [ctx.contracts.tokens.TST.address, ], + expectError: 'withdraw: balance not below threshold', + }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: 0, }, + ], +}) + + +.test({ + desc: "withdraw reverts if faucet balance is below withdrawal amount", + actions: ctx => [ + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: 0, }, + + { send: 'tokens.TST.mint', args: [ctx.contracts.TestERC20Faucet.address, et.eth(0.5)], }, + + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, et.eth(1)], }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.withdraw', args: [ctx.contracts.tokens.TST.address, ], + expectError: 'ERC20: transfer amount exceeds balance', + }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: 0, }, + ], +}) + + +.test({ + desc: "withdraw reverts if user balance is not below threshold", + actions: ctx => [ + { send: 'tokens.TST.mint', args: [ctx.contracts.TestERC20Faucet.address, et.eth(0.5)], }, + + { send: 'tokens.TST.mint', args: [ctx.wallet2.address, et.eth(0.5)], }, + + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, et.eth(0.5)], }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.withdraw', args: [ctx.contracts.tokens.TST.address, ], + expectError: 'withdraw: balance not below threshold', + }, + + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: et.eth(0.5), }, + ], +}) + + +.test({ + desc: "user can withdraw multiple tokkens", + actions: ctx => [ + { send: 'tokens.TST.mint', args: [ctx.contracts.TestERC20Faucet.address, et.eth(0.5)], }, + { send: 'tokens.TST2.mint', args: [ctx.contracts.TestERC20Faucet.address, et.eth(0.5)], }, + + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST.address, et.eth(0.25)], }, + { send: 'TestERC20Faucet.setThreshold', args: [ctx.contracts.tokens.TST2.address, et.eth(0.2)], }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.withdraw', args: [ctx.contracts.tokens.TST.address, ], }, + { call: 'tokens.TST.balanceOf', args: [ctx.wallet2.address], assertEql: et.eth(0.25), }, + + { from: ctx.wallet2, send: 'TestERC20Faucet.withdraw', args: [ctx.contracts.tokens.TST2.address, ], }, + { call: 'tokens.TST2.balanceOf', args: [ctx.wallet2.address], assertEql: et.eth(0.2), }, + + { call: 'tokens.TST.balanceOf', args: [ctx.contracts.TestERC20Faucet.address], assertEql: et.eth(0.25), }, + { call: 'tokens.TST2.balanceOf', args: [ctx.contracts.TestERC20Faucet.address], assertEql: et.eth(0.3), }, + ], +}) + + + +.run();