Skip to content

Commit

Permalink
Merge pull request #50 from Doy-lee/eip-2612-gasless-approval
Browse files Browse the repository at this point in the history
Allow EIP2612, gas-less approvals in the staking flow
  • Loading branch information
Doy-lee authored Jul 15, 2024
2 parents 25b9eba + 2f89875 commit fedbba1
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 7 deletions.
5 changes: 3 additions & 2 deletions contracts/SENT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "./libraries/Shared.sol";

/**
* @title SENT contract
* @notice The SENT utility token
*/
contract SENT is ERC20, Shared {
contract SENT is ERC20, ERC20Permit, Shared {
constructor(
uint256 totalSupply_,
address receiverGenesisAddress
) ERC20("Session", "SENT") nzAddr(receiverGenesisAddress) nzUint(totalSupply_) {
) ERC20("Session", "SENT") ERC20Permit("Session") nzAddr(receiverGenesisAddress) nzUint(totalSupply_) {
_mint(receiverGenesisAddress, totalSupply_);
}

Expand Down
31 changes: 30 additions & 1 deletion contracts/ServiceNodeContribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.20;

import "./libraries/Shared.sol";
import "./interfaces/IServiceNodeRewards.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
Expand Down Expand Up @@ -93,7 +94,6 @@ contract ServiceNodeContribution is Shared {
// State-changing functions //
// //
//////////////////////////////////////////////////////////////

/**
* @notice Allows the operator to contribute funds towards their own node.
*
Expand All @@ -120,6 +120,21 @@ contract ServiceNodeContribution is Shared {
contributeFunds(amount);
}

function contributeOperatorFundsWithPermit(
uint256 amount,
IServiceNodeRewards.BLSSignatureParams memory _blsSignature,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public onlyOperator {
// NOTE: Try catch makes the code tolerant to front-running, see:
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/05f218fb6617932e56bf5388c3b389c3028a7b73/contracts/token/ERC20/extensions/IERC20Permit.sol#L19
IERC20Permit token = IERC20Permit(address(SENT));
try token.permit(msg.sender, address(this), amount, deadline, v, r, s) {} catch {}
contributeOperatorFunds(amount, _blsSignature);
}

/**
* @notice Contribute funds to the contract for the service node run by
* `operator`. The `amount` of SENT token must be at least the
Expand Down Expand Up @@ -179,6 +194,20 @@ contract ServiceNodeContribution is Shared {
}
}

function contributeFundsWithPermit(
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public {
// NOTE: Try catch makes the code tolerant to front-running, see:
// `contributeOperatorFundsWithPermit`
IERC20Permit token = IERC20Permit(address(SENT));
try token.permit(msg.sender, address(this), amount, deadline, v, r, s) {} catch {}
contributeFunds(amount);
}

/**
* @notice Invoked when the `totalContribution` of the contract matches the
* `stakingRequirement`. The service node registration and SENT tokens are
Expand Down
20 changes: 19 additions & 1 deletion contracts/ServiceNodeRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import "./interfaces/IServiceNodeRewards.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
Expand Down Expand Up @@ -285,10 +286,27 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
BLSSignatureParams calldata blsSignature,
ServiceNodeParams calldata serviceNodeParams,
Contributor[] calldata contributors
) external whenNotPaused {
) public whenNotPaused {
_addBLSPublicKey(blsPubkey, blsSignature, msg.sender, serviceNodeParams, contributors);
}

function addBLSPublicKeyWithPermit(
BN256G1.G1Point calldata blsPubkey,
BLSSignatureParams calldata blsSignature,
ServiceNodeParams calldata serviceNodeParams,
Contributor[] calldata contributors,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external whenNotPaused {
// NOTE: Try catch makes the code tolerant to front-running, see:
// ServiceNodeContribution.sol
IERC20Permit token = IERC20Permit(address(designatedToken));
try token.permit(msg.sender, address(this), _stakingRequirement, deadline, v, r, s) {} catch {}
addBLSPublicKey(blsPubkey, blsSignature, serviceNodeParams, contributors);
}

/// @dev Internal function to add a BLS public key.
///
/// @param blsPubkey 64 byte BLS public key for the service node.
Expand Down
5 changes: 3 additions & 2 deletions contracts/test/MockERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract MockERC20 is ERC20 {
contract MockERC20 is ERC20, ERC20Permit {
uint8 immutable d;
constructor(string memory name_, string memory symbol_, uint8 _decimals) ERC20(name_, symbol_) {
constructor(string memory name_, string memory symbol_, uint8 _decimals) ERC20(name_, symbol_) ERC20Permit(name_) {
d = _decimals;
_mint(msg.sender, 1e8 * (10 ** uint256(d))); // Mint 100 million tokens for testing
}
Expand Down
3 changes: 3 additions & 0 deletions hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,8 @@ module.exports = {
},
},
},
paths: {
tests: "./test/unit-js",
},
};

98 changes: 98 additions & 0 deletions test/unit-js/ServiceNodeContributionTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ async function withdrawContributor(sentToken, snContribution, contributor) {
expect(contributorArrayExpected).to.deep.equal(contributorArray);
}

async function setupPermitSignature(token, owner, spenderAddress, amount, deadline) {
const domain = {
name: await token.name(),
version: '1',
chainId: (await ethers.provider.getNetwork()).chainId,
verifyingContract: await token.getAddress(),
};

const schema = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
};

const payload = {
owner: owner.address,
spender: spenderAddress,
value: amount,
nonce: await token.nonces(owner.address),
deadline: deadline,
};

const result = await owner.signTypedData(domain, schema, payload);
return result;
}

describe("ServiceNodeContribution Contract Tests", function () {
// NOTE: Contract factories for deploying onto the blockchain
let sentTokenContractFactory;
Expand Down Expand Up @@ -258,6 +288,74 @@ describe("ServiceNodeContribution Contract Tests", function () {
.to.be.revertedWith("Contribution is below minimum requirement");
});

it("Operator can contribute stake w/ permit", async function () {
const stake = await snContribution.stakingRequirement();

const currentBlock = await ethers.provider.getBlockNumber();
const blockTimestamp = (await ethers.provider.getBlock(currentBlock)).timestamp;
const deadline = blockTimestamp + (60 * 10); // Expire 10 minutes from now

const signature = await setupPermitSignature(sentToken, snOperator, snContributionAddress, stake, deadline);
const signatureNo0x = signature.substring(2);
const r = signatureNo0x.substring(0, 64);
const s = signatureNo0x.substring(64, 128);
const v = signatureNo0x.substring(128, 130);
await snContribution.connect(snOperator)
.contributeOperatorFundsWithPermit(stake,
[3, 4, 5, 6],
deadline,
'0x' + v,
'0x' + r,
'0x' + s);
expect(await snContribution.totalContribution()).to.equal(stake);
});

it("Operator can't contribute more than staking requirement w/ permit", async function () {
const stake = (await snContribution.stakingRequirement()) + BigInt(1);

const currentBlock = await ethers.provider.getBlockNumber();
const blockTimestamp = (await ethers.provider.getBlock(currentBlock)).timestamp;
const deadline = blockTimestamp + (60 * 10); // Expire 10 minutes from now

const signature = await setupPermitSignature(sentToken, snOperator, snContributionAddress, stake, deadline);
const signatureNo0x = signature.substring(2);
const r = signatureNo0x.substring(0, 64);
const s = signatureNo0x.substring(64, 128);
const v = signatureNo0x.substring(128, 130);
await expect(snContribution.connect(snOperator)
.contributeOperatorFundsWithPermit(stake,
[3, 4, 5, 6],
deadline,
'0x' + v,
'0x' + r,
'0x' + s)).to.be.reverted;
expect(await snContribution.totalContribution()).to.equal(BigInt(0));
});


it("Operator can't submit expired stake w/ permit", async function () {
const stake = await snContribution.stakingRequirement();
await sentToken.transfer(snOperator, stake);

const currentBlock = await ethers.provider.getBlockNumber();
const blockTimestamp = (await ethers.provider.getBlock(currentBlock)).timestamp;
const deadline = blockTimestamp - (60 * 10); // Expired 10 minutes prior

const signature = await setupPermitSignature(sentToken, snOperator, snContributionAddress, stake, deadline);
const signatureNo0x = signature.substring(2);
const r = signatureNo0x.substring(0, 64);
const s = signatureNo0x.substring(64, 128);
const v = signatureNo0x.substring(128, 130);
await expect(snContribution.connect(snOperator)
.contributeOperatorFundsWithPermit(stake,
[3, 4, 5, 6],
deadline,
'0x' + v,
'0x' + r,
'0x' + s)).to.be.reverted;
expect(await snContribution.totalContribution()).to.equal(0);
});

it("Allows operator to contribute and records correct balance", async function () {
const minContribution = await snContribution.minimumContribution();
await sentToken.transfer(snOperator, TEST_AMNT);
Expand Down
1 change: 0 additions & 1 deletion test/unit-js/ServiceNodeRewardsTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ describe("ServiceNodeRewards Contract Tests", function () {
await expect(serviceNodeRewards.connect(owner).seedPublicKeyList(pkX, pkY, amounts))
.to.be.revertedWithCustomError(serviceNodeRewards, "BLSPubkeyAlreadyExists")
});

});
});

0 comments on commit fedbba1

Please sign in to comment.