diff --git a/contracts/amo/DynamicDutyCalculator.sol b/contracts/amo/DynamicDutyCalculator.sol index cb92f171..1a50fa6b 100644 --- a/contracts/amo/DynamicDutyCalculator.sol +++ b/contracts/amo/DynamicDutyCalculator.sol @@ -66,6 +66,8 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC bytes32 public constant INTERACTION = keccak256("INTERACTION"); + bytes32 public constant BOT = keccak256("BOT"); + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -90,7 +92,7 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC minPrice = 9 * PEG / 10; maxPrice = 11 * PEG / 10; - require(_delta < (maxPrice - minPrice), "AggMonetaryPolicy/invalid-delta"); + require(_delta < (maxPrice - minPrice), "AMO/invalid-delta"); delta = _delta; _setupRole(DEFAULT_ADMIN_ROLE, _admin); @@ -105,23 +107,13 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC * @param enabled If the collateral token is enabled for the dynamic interest rate mechanism. */ function setCollateralParams(address collateral, uint256 beta, uint256 rate0, bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(collateral != address(0), "AggMonetaryPolicy/invalid-address"); - require(beta > 3e5 && beta < 1e8, "AggMonetaryPolicy/invalid-beta"); + require(collateral != address(0), "AMO/invalid-address"); + require(beta > 3e5 && beta < 1e8, "AMO/invalid-beta"); ilks[collateral].beta = beta; - ilks[collateral].rate0 = rate0; ilks[collateral].enabled = enabled; - uint256 price = oracle.peek(lisUSD); - ilks[collateral].lastPrice = price; - - uint256 duty = calculateRate(price, beta, rate0) + 1e27; - if (duty > maxDuty) duty = maxDuty; - if (duty < minDuty) duty = minDuty; - - IDao(interaction).setCollateralDuty(collateral, duty); - - emit CollateralParamsUpdated(collateral, beta, rate0, enabled); + _setCollateralRate0(collateral, beta, rate0, enabled); } /** @@ -192,8 +184,8 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC * @param _maxPrice The highest lisUSD price of the dynamic interest rate mechanism's effect. */ function setPriceRange(uint256 _minPrice, uint256 _maxPrice) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_minPrice < PEG && _maxPrice > PEG, "AggMonetaryPolicy/invalid-price-range"); - require(delta < (_maxPrice - _minPrice), "AggMonetaryPolicy/invalid-price-diff"); + require(_minPrice < PEG && _maxPrice > PEG, "AMO/invalid-price-range"); + require(delta < (_maxPrice - _minPrice), "AMO/invalid-price-diff"); minPrice = _minPrice; maxPrice = _maxPrice; @@ -207,7 +199,7 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC * @param _maxDuty The maximum duty. */ function setDutyRange(uint256 _minDuty, uint256 _maxDuty) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_minDuty >= 1e27 && _minDuty < _maxDuty, "AggMonetaryPolicy/invalid-duty-range"); + require(_minDuty >= 1e27 && _minDuty < _maxDuty, "AMO/invalid-duty-range"); minDuty = _minDuty; maxDuty = _maxDuty; @@ -220,8 +212,8 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC * @param _delta The minimum price change required to update duty dynamically. */ function setDelta(uint256 _delta) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(delta != _delta && _delta <= minPrice, "AggMonetaryPolicy/invalid-delta"); - require(_delta < (maxPrice - minPrice), "AggMonetaryPolicy/delta-is-too-large"); + require(delta != _delta && _delta <= minPrice, "AMO/invalid-delta"); + require(_delta < (maxPrice - minPrice), "AMO/delta-is-too-large"); delta = _delta; @@ -234,20 +226,20 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC * @param _addr The address to set. */ function file(bytes32 what, address _addr) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_addr != address(0), "AggMonetaryPolicy/zero-address-provided"); + require(_addr != address(0), "AMO/zero-address-provided"); if (what == "interaction") { - require(interaction != _addr, "AggMonetaryPolicy/interaction-already-set"); + require(interaction != _addr, "AMO/interaction-already-set"); revokeRole(INTERACTION, interaction); interaction = _addr; grantRole(INTERACTION, _addr); } else if (what == "lisUSD") { - require(lisUSD != _addr, "AggMonetaryPolicy/lisUSD-already-set"); + require(lisUSD != _addr, "AMO/lisUSD-already-set"); lisUSD = _addr; } else if (what == "oracle") { - require(address(oracle) != _addr, "AggMonetaryPolicy/oracle-already-set"); + require(address(oracle) != _addr, "AMO/oracle-already-set"); oracle = IResilientOracle(_addr); - } else revert("AggMonetaryPolicy/file-unrecognized-param"); + } else revert("AMO/file-unrecognized-param"); emit File(what, _addr); } @@ -281,4 +273,42 @@ contract DynamicDutyCalculator is IDynamicDutyCalculator, Initializable, AccessC return 1e18; } } + + /** + * @dev Set rate0 for a collateral tokens. + * @param collaterals The collateral token address list. + * @param rates0 The rates0 list when the price is equal to PEG. + */ + function setCollateralRate0(address[] memory collaterals, uint256[] memory rates0) external onlyRole(BOT) { + require(collaterals.length > 0 && collaterals.length == rates0.length, "AMO/invalid-params"); + + for (uint256 i = 0; i < collaterals.length; i++) { + address collateral = collaterals[i]; + require(collateral != address(0), "AMO/invalid-address"); + require(ilks[collateral].enabled, "AMO/invalid-status"); + + _setCollateralRate0(collateral, ilks[collateral].beta, rates0[i], ilks[collateral].enabled); + } + } + + function _setCollateralRate0(address collateral, uint256 beta, uint256 rate0, bool enabled) private { + ilks[collateral].rate0 = rate0; + + uint256 price = oracle.peek(lisUSD); + ilks[collateral].lastPrice = price; + + uint256 duty; + if (price <= minPrice) { + duty = maxDuty; + } else if (price >= maxPrice) { + duty = minDuty; + } else { + duty = calculateRate(price, beta, rate0) + 1e27; + if (duty > maxDuty) duty = maxDuty; + if (duty < minDuty) duty = minDuty; + } + + IDao(interaction).setCollateralDuty(collateral, duty); + emit CollateralParamsUpdated(collateral, beta, rate0, enabled); + } } diff --git a/lib/forge-std b/lib/forge-std index 8f24d6b0..1714bee7 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/scripts/upgrades/deploy_impl.js b/scripts/upgrades/deploy_impl.js index efe88a76..9adf21f7 100644 --- a/scripts/upgrades/deploy_impl.js +++ b/scripts/upgrades/deploy_impl.js @@ -3,7 +3,7 @@ const {deployImplementation, verifyImpContract} = require('./utils/upgrade_utils const oldContractAddress = '' const oldContractName = '' -const contractName = 'VaultManager' +const contractName = 'DynamicDutyCalculator' async function main() { diff --git a/test/foundry/DynamicDutyCalculator.t.sol b/test/foundry/DynamicDutyCalculator.t.sol index df418762..b199110e 100644 --- a/test/foundry/DynamicDutyCalculator.t.sol +++ b/test/foundry/DynamicDutyCalculator.t.sol @@ -20,10 +20,15 @@ contract DynamicDutyCalculatorTest is Test { uint256 delta = 200000; address public proxyAdminOwner = address(0x2A11AA); + address public bot = address(0x3A11AA); address public collateral = address(0x5A11AA); // random address + address public collateral1 = address(0x6A11AA); // random address + uint256 beta = 1e6; uint256 rate0 = 3309234382829741600; // 11% APY + uint256 rate0_15p = 4431822000000000000; // 15% APY + uint256 rate0_400p = 51034942716352291304; // 400% APY address admin; @@ -35,7 +40,7 @@ contract DynamicDutyCalculatorTest is Test { admin = msg.sender; - vm.expectRevert("AggMonetaryPolicy/invalid-delta"); + vm.expectRevert("AMO/invalid-delta"); TransparentUpgradeableProxy __dynamicDutyCalculatorProxy = new TransparentUpgradeableProxy( address(dynamicDutyCalculatorImpl), proxyAdminOwner, @@ -54,6 +59,10 @@ contract DynamicDutyCalculatorTest is Test { ) ); dynamicDutyCalculator = DynamicDutyCalculator(address(dynamicDutyCalculatorProxy)); + + vm.startPrank(msg.sender); + dynamicDutyCalculator.grantRole(dynamicDutyCalculator.BOT(), bot); + vm.stopPrank(); } function testRevert_initialize() public { @@ -107,12 +116,12 @@ contract DynamicDutyCalculatorTest is Test { dynamicDutyCalculator.setCollateralParams(collateral, beta, rate0, true); vm.startPrank(admin); - vm.expectRevert("AggMonetaryPolicy/invalid-beta"); + vm.expectRevert("AMO/invalid-beta"); dynamicDutyCalculator.setCollateralParams(collateral, 3e5, rate0, true); vm.stopPrank(); vm.startPrank(admin); - vm.expectRevert("AggMonetaryPolicy/invalid-beta"); + vm.expectRevert("AMO/invalid-beta"); dynamicDutyCalculator.setCollateralParams(collateral, 1e8, rate0, true); vm.stopPrank(); } @@ -677,7 +686,7 @@ contract DynamicDutyCalculatorTest is Test { function testRevert_setDelta() public { vm.startPrank(admin); uint256 _delta = 20000000; - vm.expectRevert("AggMonetaryPolicy/delta-is-too-large"); + vm.expectRevert("AMO/delta-is-too-large"); dynamicDutyCalculator.setDelta(_delta); vm.stopPrank(); } @@ -702,9 +711,101 @@ contract DynamicDutyCalculatorTest is Test { dynamicDutyCalculator.file("interaction", address(0xAA)); vm.startPrank(admin); - vm.expectRevert("AggMonetaryPolicy/file-unrecognized-param"); + vm.expectRevert("AMO/file-unrecognized-param"); dynamicDutyCalculator.file("proxy", address(0xAA)); vm.stopPrank(); } + + function test_setCollateralRate0() public { + test_setCollateralParams(); + + vm.mockCall( + address(oracle), + abi.encodeWithSelector(ResilientOracle.peek.selector, lisUSD), + abi.encode(uint256(99500000)) // returns $0.995 + ); + + vm.mockCall( + address(interaction), + abi.encodeWithSelector(Interaction.setCollateralDuty.selector), + abi.encode(uint256(0)) + ); + + vm.startPrank(bot); + address[] memory collaterals = new address[](1); + collaterals[0] = collateral; + uint256[] memory rates = new uint256[](1); + rates[0] = rate0_15p; + dynamicDutyCalculator.setCollateralRate0(collaterals, rates); + vm.stopPrank(); + + (bool _enabled, uint256 _lastPrice, uint256 _rate0, uint256 _beta) = dynamicDutyCalculator.ilks(collateral); + + assertEq(_beta, beta); + assertEq(_rate0, rate0_15p); + assertEq(_enabled, true); + assertEq(_lastPrice, 99500000); + } + + function test_setCollateralRate0_price_overflow() public { + test_setCollateralParams(); + vm.mockCall( + address(oracle), + abi.encodeWithSelector(ResilientOracle.peek.selector, lisUSD), + abi.encode(uint256(89000000)) // returns $0.89 + ); + + vm.mockCall( + address(interaction), + abi.encodeWithSelector(Interaction.setCollateralDuty.selector), + abi.encode(uint256(0)) + ); + + vm.startPrank(bot); + address[] memory collaterals = new address[](1); + collaterals[0] = collateral; + uint256[] memory rates = new uint256[](1); + rates[0] = rate0_15p; + dynamicDutyCalculator.setCollateralRate0(collaterals, rates); + vm.stopPrank(); + + (bool _enabled, uint256 _lastPrice, uint256 _rate0, uint256 _beta) = dynamicDutyCalculator.ilks(collateral); + + assertEq(_beta, beta); + assertEq(_rate0, rate0_15p); + assertEq(_enabled, true); + assertEq(_lastPrice, 89000000); + } + + function test_setCollateralRate0_revert() public { + test_setCollateralParams(); + + address[] memory collaterals = new address[](1); + collaterals[0] = collateral; + uint256[] memory rates = new uint256[](1); + rates[0] = rate0_15p; + + vm.startPrank(bot); + vm.expectRevert("AMO/invalid-params"); + dynamicDutyCalculator.setCollateralRate0(collaterals, new uint256[](2)); + vm.stopPrank(); + + vm.startPrank(address(1)); + vm.expectRevert("AccessControl: account 0x0000000000000000000000000000000000000001 is missing role 0x902cbe3a02736af9827fb6a90bada39e955c0941e08f0c63b3a662a7b17a4e2b"); + dynamicDutyCalculator.setCollateralRate0(collaterals, rates); + vm.stopPrank(); + + collaterals[0] = address(0); + vm.startPrank(bot); + vm.expectRevert("AMO/invalid-address"); + dynamicDutyCalculator.setCollateralRate0(collaterals, rates); + vm.stopPrank(); + + collaterals[0] = collateral1; + vm.startPrank(bot); + vm.expectRevert("AMO/invalid-status"); + dynamicDutyCalculator.setCollateralRate0(collaterals, rates); + vm.stopPrank(); + } }