Skip to content

Commit

Permalink
feat: adding sync public method
Browse files Browse the repository at this point in the history
  • Loading branch information
wei3erHase committed Aug 21, 2024
1 parent 52a451e commit a2b599f
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 51 deletions.
21 changes: 21 additions & 0 deletions solidity/contracts/DataReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ contract DataReceiver is IDataReceiver, Governable {
}
}

function syncObservations(bytes32 _poolSalt, uint256 _maxObservations) external {
IOracleSidechain _oracle = deployedOracles[_poolSalt];
if (address(_oracle) == address(0)) revert ZeroAddress();
uint24 _currentNonce = _oracle.poolNonce();
IOracleSidechain.ObservationData[] memory _cachedObservationsData = _cachedObservations[_poolSalt][_currentNonce];
if (_cachedObservationsData.length == 0) revert ObservationsNotWritable();
uint256 _i;
while (_maxObservations == 0 || _i < _maxObservations) {
_cachedObservationsData = _cachedObservations[_poolSalt][_currentNonce];
if (_cachedObservationsData.length > 0) {
_oracle.write(_cachedObservationsData, _currentNonce);
emit ObservationsAdded(_poolSalt, _currentNonce, msg.sender);
delete _cachedObservations[_poolSalt][_currentNonce];
_currentNonce++;
_i++;
} else {
break;
}
}
}

function whitelistAdapter(IBridgeReceiverAdapter _receiverAdapter, bool _isWhitelisted) external onlyGovernor {
_whitelistAdapter(_receiverAdapter, _isWhitelisted);
}
Expand Down
126 changes: 122 additions & 4 deletions test/e2e/data-receiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('@skip-on-coverage DataReceiver.sol', () => {
let tx: ContractTransaction;
let snapshotId: string;

const nonce = 1;
const nonce = 42;

before(async () => {
await evm.reset({
Expand Down Expand Up @@ -69,12 +69,15 @@ describe('@skip-on-coverage DataReceiver.sol', () => {
});

context('when an oracle is registered', () => {
let caller: string;

beforeEach(async () => {
await dataReceiver.addObservations([[0, 0]] as unknown as IOracleSidechain.ObservationDataStruct[], salt, nonce - 1);
caller = connextReceiverAdapter.address;
await dataReceiver.addObservations([[0, 0]] as IOracleSidechain.ObservationDataStructOutput[], salt, nonce);
});

it('should add the observations', async () => {
tx = await dataReceiver.addObservations(observationsData, salt, nonce);
tx = await dataReceiver.addObservations(observationsData, salt, nonce + 1);

({ oracleSidechain } = await getOracle(oracleFactory.address, tokenA.address, tokenB.address, fee));
const slot0 = await oracleSidechain.slot0();
Expand All @@ -84,7 +87,49 @@ describe('@skip-on-coverage DataReceiver.sol', () => {
.to.emit(oracleSidechain, 'Swap')
.withArgs(...SWAP_EVENT_ARGS);

await expect(tx).to.emit(dataReceiver, 'ObservationsAdded').withArgs(salt, nonce, connextReceiverAdapter.address);
await expect(tx)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 1, caller);
});

it('should remember observations that arrived disordered', async () => {
const obs1 = [[1, 100]] as IOracleSidechain.ObservationDataStructOutput[];
const obs2 = [[2, 100]] as IOracleSidechain.ObservationDataStructOutput[];
const obs3 = [[3, 100]] as IOracleSidechain.ObservationDataStructOutput[];
const obs4 = [[4, 100]] as IOracleSidechain.ObservationDataStructOutput[];

/*
- Creates a setup in which an observation arrives (3) before the previous nonce (2) has been processed.
- In this case, the observation should be cached and processed later (as long as the previous nonce is already processed).
- In this setup, observations arrive in the order (1), (3), (2), (4) and expected to be processed in numerical order.
- This will happen in the following way:
- (1) is processed immediately.
- then (3) is cached (current is 1).
- (2) is processed immediately.
- (4) is cached (current is 2).
- (3) is processed.
- (4) is processed (current is 3).
*/

const tx1 = await dataReceiver.addObservations(obs1, salt, nonce + 1); // initial
const tx2 = await dataReceiver.addObservations(obs3, salt, nonce + 3); // disordered (before 2)
const tx3 = await dataReceiver.addObservations(obs2, salt, nonce + 2); // ordered (after 1)
const tx4 = await dataReceiver.addObservations(obs4, salt, nonce + 4); // should include 3

await expect(tx1)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 1, caller);
await expect(tx2).to.emit(dataReceiver, 'ObservationsCached');
await expect(tx2).not.to.emit(dataReceiver, 'ObservationsAdded');
await expect(tx3)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 2, caller);
await expect(tx4)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 3, caller);
await expect(tx4)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 4, caller);
});
});

Expand Down Expand Up @@ -122,4 +167,77 @@ describe('@skip-on-coverage DataReceiver.sol', () => {
});
});
});

describe('syncing observations', () => {
let connextReceiverAdapterSigner: JsonRpcSigner;
let dataReceiverSigner: JsonRpcSigner;
let blockTimestamp1 = 1000000;
let tick1 = 100;
let observationData1 = [blockTimestamp1, tick1] as IOracleSidechain.ObservationDataStructOutput;
let blockTimestamp2 = 3000000;
let tick2 = 300;
let observationData2 = [blockTimestamp2, tick2] as IOracleSidechain.ObservationDataStructOutput;
let observationsData = [observationData1, observationData2];

beforeEach(async () => {
connextReceiverAdapterSigner = await wallet.impersonate(connextReceiverAdapter.address);
dataReceiverSigner = await wallet.impersonate(dataReceiver.address);
await wallet.setBalance(connextReceiverAdapter.address, toUnit(10));
await wallet.setBalance(dataReceiver.address, toUnit(10));
dataReceiver = dataReceiver.connect(connextReceiverAdapterSigner);
oracleFactory = oracleFactory.connect(dataReceiverSigner);
});

context('when an oracle is registered', () => {
let caller: string;

beforeEach(async () => {
caller = connextReceiverAdapter.address;
await dataReceiver.addObservations([[0, 0]] as IOracleSidechain.ObservationDataStructOutput[], salt, nonce - 2);
});

context('when the cache at pool nonce is empty', () => {
it('should do nothing', async () => {
await expect(dataReceiver.syncObservations(salt, 0)).to.be.revertedWith('ObservationsNotWritable()');
});
});

context('when the cache is populated', () => {
beforeEach(async () => {
await dataReceiver.addObservations([[0, 0]] as IOracleSidechain.ObservationDataStructOutput[], salt, nonce + 2);
await dataReceiver.addObservations([[1, 0]] as IOracleSidechain.ObservationDataStructOutput[], salt, nonce + 1);
await dataReceiver.addObservations([[2, 0]] as IOracleSidechain.ObservationDataStructOutput[], salt, nonce);
await dataReceiver.addObservations([[3, 0]] as IOracleSidechain.ObservationDataStructOutput[], salt, nonce - 1);
// NOTE: dataReceiver should be at poolNonce == nonce by now (with nonce, nonce+1 & nonce+2 in cache)
});

it('should all the observations when called without max', async () => {
tx = await dataReceiver.syncObservations(salt, 0);

await expect(tx).to.emit(dataReceiver, 'ObservationsAdded').withArgs(salt, nonce, caller);
await expect(tx)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 1, caller);
await expect(tx)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 2, caller);
});

it('should all observations limited by max argument', async () => {
tx = await dataReceiver.syncObservations(salt, 2);

await expect(tx).to.emit(dataReceiver, 'ObservationsAdded').withArgs(salt, nonce, caller);
await expect(tx)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(salt, nonce + 1, caller);
});
});
});

context('when an oracle is not registered', () => {
it('should revert', async () => {
await expect(dataReceiver.syncObservations(salt, 0)).to.be.revertedWith('ZeroAddress()');
});
});
});
});
32 changes: 0 additions & 32 deletions test/e2e/oracle-sidechain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,36 +91,4 @@ describe('@skip-on-coverage OracleSidechain.sol', () => {
expect(observation[3]); // initialized
});
});

it('should remember observations that arrived disordered', async () => {
const obs1 = [[1, 100]] as IOracleSidechain.ObservationDataStructOutput[];
const obs2 = [[2, 100]] as IOracleSidechain.ObservationDataStructOutput[];
const obs3 = [[3, 100]] as IOracleSidechain.ObservationDataStructOutput[];
const obs4 = [[4, 100]] as IOracleSidechain.ObservationDataStructOutput[];

/*
- Creates a setup in which an observation arrives (3) before the previous nonce (2) has been processed.
- In this case, the observation should be cached and processed later (as long as the previous nonce is already processed).
- In this setup, observations arrive in the order (1), (3), (2), (4) and expected to be processed in numerical order.
- This will happen in the following way:
- (1) is processed immediately.
- then (3) is cached (current is 1).
- (2) is processed immediately.
- (4) is cached (current is 2).
- (3) is processed.
- (4) is processed (current is 3).
*/

const tx1 = await allowedDataReceiver.internalAddObservations(obs1, salt, 1); // initial
const tx2 = await allowedDataReceiver.internalAddObservations(obs3, salt, 3); // disordered (before 2)
const tx3 = await allowedDataReceiver.internalAddObservations(obs2, salt, 2); // ordered (after 1)
const tx4 = await allowedDataReceiver.internalAddObservations(obs4, salt, 4); // should include 3

await expect(tx1).to.emit(allowedDataReceiver, 'ObservationsAdded').withArgs(salt, 1, deployer.address);
await expect(tx2).to.emit(allowedDataReceiver, 'ObservationsCached');
await expect(tx2).not.to.emit(allowedDataReceiver, 'ObservationsAdded');
await expect(tx3).to.emit(allowedDataReceiver, 'ObservationsAdded').withArgs(salt, 2, deployer.address);
await expect(tx4).to.emit(allowedDataReceiver, 'ObservationsAdded').withArgs(salt, 3, deployer.address);
await expect(tx4).to.emit(allowedDataReceiver, 'ObservationsAdded').withArgs(salt, 4, deployer.address);
});
});
108 changes: 100 additions & 8 deletions test/unit/DataReceiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('DataReceiver.sol', () => {
let governor: SignerWithAddress;
let fakeAdapter: SignerWithAddress;
let randomAdapter: SignerWithAddress;
let randomWallet: SignerWithAddress;
let dataReceiver: MockContract<DataReceiver>;
let dataReceiverFactory: MockContractFactory<DataReceiver__factory>;
let oracleFactory: FakeContract<IOracleFactory>;
Expand All @@ -27,7 +28,7 @@ describe('DataReceiver.sol', () => {
const randomNonce = 420;

before(async () => {
[, governor, fakeAdapter, randomAdapter] = await ethers.getSigners();
[randomWallet, governor, fakeAdapter, randomAdapter] = await ethers.getSigners();

oracleFactory = await smock.fake('IOracleFactory');
oracleSidechain = await smock.fake('IOracleSidechain');
Expand Down Expand Up @@ -57,13 +58,10 @@ describe('DataReceiver.sol', () => {
});

describe('addObservations(...)', () => {
let blockTimestamp1 = 1000000;
let tick1 = 100;
let observationData1 = [blockTimestamp1, tick1];
let blockTimestamp2 = 3000000;
let tick2 = 300;
let observationData2 = [blockTimestamp2, tick2];
let observationsData = [observationData1, observationData2];
let observationsData = [
[1000000, 100],
[300, 3000000],
] as IOracleSidechain.ObservationDataStructOutput[];

beforeEach(async () => {
await dataReceiver.connect(governor).whitelistAdapter(fakeAdapter.address, true);
Expand Down Expand Up @@ -230,6 +228,7 @@ describe('DataReceiver.sol', () => {
await dataReceiver.connect(fakeAdapter).addObservations(observationsData, randomSalt, randomNonce - 1);
await dataReceiver.connect(fakeAdapter).addObservations(observationsData, randomSalt, randomNonce - 2);
oracleSidechain.poolNonce.whenCalledWith().returns(randomNonce - 2);
// oracleSidechain is at poolNonce = randomNonce - 2 and cache is populated with -2, -1, +1 , +2 (nonce is empty)

oracleSidechain.write.reset();
});
Expand Down Expand Up @@ -285,6 +284,99 @@ describe('DataReceiver.sol', () => {
});
});

describe('syncObservations(...)', () => {
const observationsDataA = [[1, 10]] as IOracleSidechain.ObservationDataStructOutput[];
const observationsDataB = [[2, 20]] as IOracleSidechain.ObservationDataStructOutput[];
const observationsDataC = [[3, 30]] as IOracleSidechain.ObservationDataStructOutput[];

let caller: string;

beforeEach(async () => {
oracleSidechain.poolNonce.whenCalledWith().returns(randomNonce);
caller = randomWallet.address;
});

context('when an oracle is registered', () => {
beforeEach(async () => {
await dataReceiver.setVariable('deployedOracles', { [randomSalt]: oracleSidechain.address });
});

it('should revert when the cache is empty at the oracle nonce', async () => {
await expect(dataReceiver.syncObservations(randomSalt, 0)).to.be.revertedWith('ObservationsNotWritable()');
});

context('when the cache is populated', () => {
beforeEach(async () => {
// Cache observations
// NOTE: smock doesn't support setting internal mapping(bytes32 => mapping(uint => Struct)) (yet)
oracleSidechain.poolNonce.whenCalledWith().returns(0);
await dataReceiver.connect(governor).whitelistAdapter(fakeAdapter.address, true);
await dataReceiver.connect(fakeAdapter).addObservations(observationsDataA, randomSalt, randomNonce);
await dataReceiver.connect(fakeAdapter).addObservations(observationsDataB, randomSalt, randomNonce + 1);
await dataReceiver.connect(fakeAdapter).addObservations(observationsDataC, randomSalt, randomNonce + 2);
oracleSidechain.poolNonce.whenCalledWith().returns(randomNonce);
// oracleSidechain is at poolNonce = randomNonce and cache is populated with nonce, +1, +2

oracleSidechain.write.reset();
});

it('should revert when the cache at pool nonce is empty', async () => {
oracleSidechain.poolNonce.whenCalledWith().returns(randomNonce + 42);
await expect(dataReceiver.syncObservations(randomSalt, 0)).to.be.revertedWith('ObservationsNotWritable()');
});

it('should all observations limited by max argument', async () => {
tx = await dataReceiver.syncObservations(randomSalt, 2);

expect(oracleSidechain.write).to.have.been.calledWith(observationsDataA, randomNonce);
expect(oracleSidechain.write).to.have.been.calledWith(observationsDataB, randomNonce + 1);
expect(oracleSidechain.write).not.to.have.been.calledWith(observationsDataC, randomNonce + 2);

await expect(tx).to.emit(dataReceiver, 'ObservationsAdded').withArgs(randomSalt, randomNonce, caller);
await expect(tx)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(randomSalt, randomNonce + 1, caller);
});

it('should all the observations when called without max', async () => {
tx = await dataReceiver.syncObservations(randomSalt, 0);

expect(oracleSidechain.write).to.have.been.calledWith(observationsDataA, randomNonce);
expect(oracleSidechain.write).to.have.been.calledWith(observationsDataB, randomNonce + 1);
expect(oracleSidechain.write).to.have.been.calledWith(observationsDataC, randomNonce + 2);

await expect(tx).to.emit(dataReceiver, 'ObservationsAdded').withArgs(randomSalt, randomNonce, caller);
await expect(tx)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(randomSalt, randomNonce + 1, caller);
await expect(tx)
.to.emit(dataReceiver, 'ObservationsAdded')
.withArgs(randomSalt, randomNonce + 2, caller);
});

it.skip('should delete added cache observations', async () => {
// should sync nonce and nonce + 1 (nonce + 2 should remain in cache)
await dataReceiver.syncObservations(randomSalt, 2);

// NOTE: smock having issues with internal mapping structs
const postCacheA = await dataReceiver.getVariable('_cachedObservations', [randomSalt, randomNonce.toString()]);
const postCacheB = await dataReceiver.getVariable('_cachedObservations', [randomSalt, (randomNonce + 1).toString()]);
const postCacheC = await dataReceiver.getVariable('_cachedObservations', [randomSalt, (randomNonce + 2).toString()]);

expect(postCacheA).to.be.undefined;
expect(postCacheB).to.be.undefined;
expect(postCacheC).to.deep.eq(observationsDataC);
});
});
});

context('when an oracle is not registered', () => {
it('should revert', async () => {
await expect(dataReceiver.syncObservations(randomSalt, 0)).to.be.revertedWith('ZeroAddress()');
});
});
});

describe('whitelistAdapter(...)', () => {
onlyGovernor(
() => dataReceiver,
Expand Down
Loading

0 comments on commit a2b599f

Please sign in to comment.