Skip to content

Commit

Permalink
Add rate limit (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
zpetersen-paxos authored Oct 30, 2024
1 parent 826e367 commit 168b4c3
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 18 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# cross-chain-contracts-internal
This repository contains contracts to support cross chain bridging on EVM chains. The main contract is
[OFTWrapper](contracts/OFTWrapper.sol) which supports bridging using LayerZero.

## OFTWrapper
This contract is a proxy around LayerZero's OFT standard. The `OFTWrapper` can be called
by LayerZero to mint and burn tokens like normal. The `OFTWrapper` then forwards those requests
to the underlying token. The underlying token must grant this contract permission to mint and burn.

## Upgrade process
`OFTWrapper` is a non-upgradeable contract. If a change needs to be made a new contract should be deployed.
The underlying token should then revoke mint and burn permissions on the old contract and grant mint and burn
permissions to the new contract.

## Contract Tests
Install dependencies:

`npm install`

Compile the contracts:

`npx hardhat compile`

Run unit tests:

`npx hardhat test`

Check test coverage:

`npx hardhat coverage`
19 changes: 17 additions & 2 deletions contracts/OFTWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
pragma solidity ^0.8.20;

import {OFTCore, IOFT} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTCore.sol";
import {RateLimiter} from "@layerzerolabs/oapp-evm/contracts/oapp/utils/RateLimiter.sol";
import {PaxosTokenV2} from "PaxosToken/contracts/PaxosTokenV2.sol";

/**
* @title OFTWrapper
* @dev This contract is a proxy around LayerZero's OFT standard. The OFTWrapper can be called
* by LayerZero to mint and burn tokens like normal. The OFTWrapper then forwards those requests
* to the underlying token. The underlying token must grant this contract permission to mint and burn.
* @custom:security-contact [email protected]
*/
contract OFTWrapper is OFTCore {
contract OFTWrapper is OFTCore, RateLimiter {
//The Paxos token
PaxosTokenV2 private immutable paxosToken;

Expand All @@ -24,9 +26,11 @@ contract OFTWrapper is OFTCore {
constructor(
address _paxosToken,
address _lzEndpoint,
address _delegate
address _delegate,
RateLimitConfig[] memory _rateLimitConfigs
) OFTCore(6, _lzEndpoint, _delegate) {
paxosToken = PaxosTokenV2(_paxosToken);
_setRateLimits(_rateLimitConfigs);
}

/**
Expand Down Expand Up @@ -55,6 +59,16 @@ contract OFTWrapper is OFTCore {
return false;
}

/**
* @dev Sets the rate limits based on RateLimitConfig array. Only callable by the owner.
* @param _rateLimitConfigs An array of RateLimitConfig structures defining the rate limits.
*/
function setRateLimits(
RateLimitConfig[] calldata _rateLimitConfigs
) external onlyOwner {
_setRateLimits(_rateLimitConfigs);
}

/**
* @dev Internal function to perform a debit operation. Calls the underlying token
* to perform the burn via `decreaseSupplyFromAddress`.
Expand All @@ -80,6 +94,7 @@ contract OFTWrapper is OFTCore {
_minAmountLD,
_dstEid
);
_checkAndUpdateRateLimit(_dstEid, amountSentLD);
paxosToken.decreaseSupplyFromAddress(amountSentLD, _from);
}

Expand Down
5 changes: 3 additions & 2 deletions contracts/fixture/OFTWrapperFixture.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ contract OFTWrapperFixture is OFTWrapper {
constructor(
address _tokenAddress,
address _lzEndpoint,
address _delegate
) OFTWrapper(_tokenAddress, _lzEndpoint, _delegate) {
address _delegate,
RateLimitConfig[] memory _rateLimitConfigs
) OFTWrapper(_tokenAddress, _lzEndpoint, _delegate, _rateLimitConfigs) {
}

//Used to test the _debit internal function directly
Expand Down
23 changes: 20 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"license": "MIT",
"devDependencies": {
"@layerzerolabs/lz-v2-utilities": "^2.3.42",
"@layerzerolabs/oapp-evm": "^0.0.4",
"@layerzerolabs/oft-evm": "^0.0.8",
"@layerzerolabs/toolbox-hardhat": "^0.3.7",
"@nomicfoundation/hardhat-toolbox": "^2.0.2",
Expand Down
11 changes: 9 additions & 2 deletions scripts/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ const { ethers } = require("hardhat");
const { PrintDeployerDetails, PrintContractDetails } = require('./utils');
const { ValidateEnvironmentVariables } = require('./utils/helpers');

const { TOKEN_ADDRESS, ETH_ENDPOINT_CONTRACT_ADDRESS, DELEGATE_ADDRESS } = process.env;
const { TOKEN_ADDRESS, ETH_ENDPOINT_CONTRACT_ADDRESS, DELEGATE_ADDRESS, DESTINATION_EID, LIMIT, WINDOW } = process.env;

const OFT_WRAPPER_CONTRACT_NAME = "OFTWrapper";

const rateLimitConfig = {
dstEid: DESTINATION_EID,
limit: LIMIT,
window: WINDOW
}

const initializerArgs = [
TOKEN_ADDRESS,
ETH_ENDPOINT_CONTRACT_ADDRESS,
DELEGATE_ADDRESS,
[rateLimitConfig]
]

async function main() {
ValidateEnvironmentVariables(initializerArgs)
ValidateEnvironmentVariables(TOKEN_ADDRESS, ETH_ENDPOINT_CONTRACT_ADDRESS, DELEGATE_ADDRESS, DESTINATION_EID, LIMIT, WINDOW )
PrintDeployerDetails();

console.log("\nDeploying Implementation contract...")
Expand Down
12 changes: 9 additions & 3 deletions scripts/deployWithFixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ const { ethers } = require("hardhat");
const { PrintDeployerDetails, PrintContractDetails } = require('./utils');
const { ValidateEnvironmentVariables } = require('./utils/helpers');

const { DELEGATE_ADDRESS } = process.env;
const { DELEGATE_ADDRESS, DESTINATION_EID, LIMIT, WINDOW } = process.env;

const OFT_WRAPPER_CONTRACT_NAME = "OFTWrapper";
const PAXOS_TOKEN_FIXTURE = "PaxosTokenFixture"
const LZ_ENDPOINT_FIXTURE = "LzEndpointFixture"

const rateLimitConfig = {
dstEid: DESTINATION_EID,
limit: LIMIT,
window: WINDOW
}

//Only used for local testing or sepolia.
async function main() {
ValidateEnvironmentVariables([DELEGATE_ADDRESS])
ValidateEnvironmentVariables([DELEGATE_ADDRESS, DESTINATION_EID, LIMIT, WINDOW])
PrintDeployerDetails();
const tokenContractFactoryImplementation = await ethers.getContractFactory(PAXOS_TOKEN_FIXTURE);
let tokenContractImplementation = await tokenContractFactoryImplementation.deploy();
Expand All @@ -22,7 +28,7 @@ async function main() {

console.log("\nDeploying Implementation contract...")
const contractFactoryImplementation = await ethers.getContractFactory(OFT_WRAPPER_CONTRACT_NAME);
let contractImplementation = await contractFactoryImplementation.deploy(tokenContractImplementation.address, endpointContractImplementation.address, DELEGATE_ADDRESS);
let contractImplementation = await contractFactoryImplementation.deploy(tokenContractImplementation.address, endpointContractImplementation.address, DELEGATE_ADDRESS, [rateLimitConfig]);
PrintContractDetails(contractImplementation, OFT_WRAPPER_CONTRACT_NAME);

}
Expand Down
47 changes: 45 additions & 2 deletions test/OFTProxyBasicTest.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const { deployOFTWrapper } = require('./helpers/fixtures');
const { deployOFTWrapper, LIMIT } = require('./helpers/fixtures');
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { assert } = require('chai');
const { assert, expect } = require('chai');

const CREDIT_AMOUNT = 10000;
const DEBIT_AMOUNT = 3000;
const NEW_LIMIT = ethers.utils.parseEther('900')

describe('OFTWrapper Test', function () {

Expand Down Expand Up @@ -32,6 +33,30 @@ describe('OFTWrapper Test', function () {
});
});

describe('setRateLimits', function () {
it('can succesfully call setRateLimits', async function () {
const newRateLimitConfig = {
dstEid: 1,
limit: NEW_LIMIT,
window: 100
}
await this.contract.setRateLimits([newRateLimitConfig])
const rateLimitConfig = await this.contract.rateLimits(newRateLimitConfig.dstEid);
console.log(rateLimitConfig)
assert.equal(rateLimitConfig.limit.toString(), newRateLimitConfig.limit.toString())
assert.equal(rateLimitConfig.window, newRateLimitConfig.window)
});

it('cannot call setRateLimits from non owner', async function () {
const newRateLimitConfig = {
dstEid: 1,
limit: NEW_LIMIT,
window: 100
}
await expect(this.contract.connect(this.admin).setRateLimits([newRateLimitConfig])).to.be.reverted;
});
});

describe('credit', function () {
it('can succesfully call credit', async function () {
await this.contract.credit(this.owner.address, CREDIT_AMOUNT, 1);
Expand All @@ -48,6 +73,24 @@ describe('OFTWrapper Test', function () {
let amount = await this.tokenFixture.balanceOf(this.owner.address)
assert.equal(amount, CREDIT_AMOUNT - DEBIT_AMOUNT)
});

it('cannot call debit if rate limit exceeded', async function () {
await this.contract.credit(this.owner.address, NEW_LIMIT + 100, 1);
await expect(this.contract.debit(this.owner.address, NEW_LIMIT + 100, 100, 1)).to.be.revertedWithCustomError(this.contract, "RateLimitExceeded")
});

it('can call debit if rate limit updated to higher amount', async function () {
await this.contract.credit(this.owner.address, NEW_LIMIT + 100, 1);
const newRateLimitConfig = {
dstEid: 1,
limit: NEW_LIMIT + NEW_LIMIT,
window: 100
}
await this.contract.setRateLimits([newRateLimitConfig])
await this.contract.debit(this.owner.address, NEW_LIMIT + 100, 100, 1)
let amount = await this.tokenFixture.balanceOf(this.owner.address)
assert.equal(amount, 0)
});
});

});
14 changes: 12 additions & 2 deletions test/OFTProxySendTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,18 @@ describe('OFTWrapper Send Test', function () {
mockEndpointA = await EndpointV2Mock.deploy(eidA);
mockEndpointB = await EndpointV2Mock.deploy(eidB);
// Deploying two instances of MyOFT contract and linking them to the mock LZEndpoint
myOFTA = await MyOFT.connect(ownerA).deploy(tokenFixture.address, mockEndpointA.address, ownerA.address)
myOFTB = await MyOFT.connect(ownerB).deploy(tokenFixtureB.address, mockEndpointB.address, ownerB.address)
const rateLimitConfigA = {
dstEid: 2,
limit: ethers.utils.parseEther('1000'),
window: 100
}
const rateLimitConfigB = {
dstEid: 1,
limit: ethers.utils.parseEther('1000'),
window: 100
}
myOFTA = await MyOFT.connect(ownerA).deploy(tokenFixture.address, mockEndpointA.address, ownerA.address, [rateLimitConfigA])
myOFTB = await MyOFT.connect(ownerB).deploy(tokenFixtureB.address, mockEndpointB.address, ownerB.address, [rateLimitConfigB])

// Setting destination endpoints in the LZEndpoint mock for each MyOFT instance
await mockEndpointA.setDestLzEndpoint(myOFTB.address, mockEndpointB.address);
Expand Down
10 changes: 8 additions & 2 deletions test/helpers/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@ const { ethers } = require("hardhat");
const PAXOS_TOKEN_FIXTURE = "PaxosTokenFixture"
const LZ_ENDPOINT_FIXTURE = "LzEndpointFixture"
const OFT_WRAPPER = "OFTWrapperFixture"
const LIMIT = ethers.utils.parseEther('1000')

async function deployOFTWrapper() {
const [owner, admin, recipient, assetProtectionRole, acc, acc2, acc3] = await ethers.getSigners();
const rateLimitConfig = {
dstEid: 1,
limit: LIMIT,
window: 100
}
const lzEndpoint = await ethers.deployContract(LZ_ENDPOINT_FIXTURE, [1, acc2.address])
const tokenFixture = await ethers.deployContract(PAXOS_TOKEN_FIXTURE)
const contract = await ethers.deployContract(OFT_WRAPPER, [tokenFixture.address, lzEndpoint.address, acc2.address])
const contract = await ethers.deployContract(OFT_WRAPPER, [tokenFixture.address, lzEndpoint.address, acc2.address, [rateLimitConfig]])
return { owner, admin, recipient, acc, acc2, acc3, assetProtectionRole, contract, tokenFixture }

}

module.exports = {
deployOFTWrapper
deployOFTWrapper,
}

0 comments on commit 168b4c3

Please sign in to comment.