diff --git a/contracts/Orchestrator.sol b/contracts/Orchestrator.sol index 4ef4b16..1a7529e 100644 --- a/contracts/Orchestrator.sol +++ b/contracts/Orchestrator.sol @@ -24,10 +24,12 @@ contract Orchestrator is Ownable { IUFragmentsPolicy public policy; + address public rebaseCaller; + /** * @param policy_ Address of the UFragments policy. */ - constructor(address policy_) public { + constructor(address policy_) { Ownable.initialize(msg.sender); policy = IUFragmentsPolicy(policy_); } @@ -35,13 +37,12 @@ contract Orchestrator is Ownable { /** * @notice Main entry point to initiate a rebase operation. * The Orchestrator calls rebase on the policy and notifies downstream applications. - * Contracts are guarded from calling, to avoid flash loan attacks on liquidity - * providers. + * Access is restricted to the authorized rebase caller to prevent unauthorized rebases. * If a transaction in the transaction list fails, Orchestrator will stop execution * and revert to prevent a gas underprice attack. */ function rebase() external { - require(msg.sender == tx.origin); // solhint-disable-line avoid-tx-origin + require(msg.sender == rebaseCaller, "Unauthorized rebase caller"); policy.rebase(); @@ -56,6 +57,15 @@ contract Orchestrator is Ownable { } } + /** + * @notice Sets the authorized rebase caller address. + * @param rebaseCaller_ Address authorized to call rebase. + */ + function setRebaseCaller(address rebaseCaller_) external onlyOwner { + require(rebaseCaller_ != address(0), "Invalid rebase caller"); + rebaseCaller = rebaseCaller_; + } + /** * @notice Adds a transaction that gets called for a downstream receiver of rebases * @param destination Address of contract destination diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 0ea1469..4612bd6 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -25,11 +25,13 @@ task('deploy:amplforce:testnet', 'Deploy ampleforth contract suite for testnet') const RATE_REPORT_EXPIRATION_SEC = 86400 // 1 day const RATE_REPORT_DELAY_SEC = 0 const RATE_MIN_PROVIDERS = 1 + const RATE_ORACLE_SCALAR = utils.parseUnits('1', 18) // CPI oracle const CPI_REPORT_EXPIRATION_SEC = 7776000 // 90 days const CPI_REPORT_DELAY_SEC = 0 const CPI_MIN_PROVIDERS = 1 + const CPI_ORACLE_SCALAR = utils.parseUnits('1', 18) // Policy const DEVIATION_TRESHOLD = utils.parseUnits('0.002', 18) // 0.002% (ie) 0.05/24) @@ -70,6 +72,7 @@ task('deploy:amplforce:testnet', 'Deploy ampleforth contract suite for testnet') RATE_REPORT_EXPIRATION_SEC, RATE_REPORT_DELAY_SEC, RATE_MIN_PROVIDERS, + RATE_ORACLE_SCALAR, ) console.log('Market oracle to:', marketOracle.address) @@ -79,6 +82,7 @@ task('deploy:amplforce:testnet', 'Deploy ampleforth contract suite for testnet') CPI_REPORT_EXPIRATION_SEC, CPI_REPORT_DELAY_SEC, CPI_MIN_PROVIDERS, + CPI_ORACLE_SCALAR, ) console.log('CPI oracle to:', cpiOracle.address) @@ -107,6 +111,10 @@ task('deploy:amplforce:testnet', 'Deploy ampleforth contract suite for testnet') ) console.log('Orchestrator deployed to:', orchestrator.address) + // Set rebase caller + await waitFor(orchestrator.connect(deployer).setRebaseCaller(owner)) + console.log('Rebase caller set to:', owner) + // Set references await waitFor(ampl.connect(deployer).setMonetaryPolicy(policy.address)) await waitFor( diff --git a/test/unit/Orchestrator.ts b/test/unit/Orchestrator.ts index 6744b1e..a144add 100644 --- a/test/unit/Orchestrator.ts +++ b/test/unit/Orchestrator.ts @@ -26,6 +26,7 @@ async function mockedOrchestrator() { ) .connect(deployer) .deploy() + await orchestrator.connect(deployer).setRebaseCaller(await deployer.getAddress()) return { deployer, user, @@ -37,7 +38,7 @@ async function mockedOrchestrator() { describe('Orchestrator', function () { before('setup Orchestrator contract', async () => { - ;({ deployer, user, orchestrator, mockPolicy, mockDownstream } = + ; ({ deployer, user, orchestrator, mockPolicy, mockDownstream } = await waffle.loadFixture(mockedOrchestrator)) }) @@ -48,8 +49,8 @@ describe('Orchestrator', function () { }) }) - describe('when rebase called by a contract', function () { - it('should fail', async function () { + describe('when rebase called by unauthorized address', function () { + it('should fail when called by a contract', async function () { const rebaseCallerContract = await ( await ethers.getContractFactory('RebaseCallerContract') ) @@ -58,6 +59,12 @@ describe('Orchestrator', function () { await expect(rebaseCallerContract.callRebase(orchestrator.address)).to.be .reverted }) + + it('should fail when called by unauthorized user', async function () { + await expect(orchestrator.connect(user).rebase()).to.be.revertedWith( + 'Unauthorized rebase caller', + ) + }) }) describe('when rebase called by a contract which is being constructed', function () { @@ -344,6 +351,30 @@ describe('Orchestrator', function () { }) }) + describe('setRebaseCaller', async function () { + it('should be callable by owner', async function () { + const newCaller = await user.getAddress() + await expect( + orchestrator.connect(deployer).setRebaseCaller(newCaller), + ).to.not.be.reverted + expect(await orchestrator.rebaseCaller()).to.eq(newCaller) + }) + + it('should not be callable by others', async function () { + const newCaller = await user.getAddress() + await expect(orchestrator.connect(user).setRebaseCaller(newCaller)).to + .be.reverted + }) + + it('should reject zero address', async function () { + await expect( + orchestrator + .connect(deployer) + .setRebaseCaller('0x0000000000000000000000000000000000000000'), + ).to.be.revertedWith('Invalid rebase caller') + }) + }) + describe('transferOwnership', async function () { it('should transfer ownership', async function () { expect(await orchestrator.owner()).to.eq(await deployer.getAddress())