Skip to content

Commit

Permalink
Merge pull request #3 from v3xlabs/backend
Browse files Browse the repository at this point in the history
Backend
  • Loading branch information
JustAnotherDevv authored Nov 16, 2024
2 parents 88e862d + 0539723 commit 680a715
Show file tree
Hide file tree
Showing 13 changed files with 2,211 additions and 244 deletions.
37 changes: 16 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ENS Auto Renewal
# Intent-Based ENS Solver Network For ENS Auto-Renewals

## Frontend

Expand All @@ -19,38 +19,33 @@ Current flow:

## API Endpoints

### POST /verify
### Add new intent

Verifies signature and token balance plus approvals for name renewal request. If both are correct, it adds the transaction to the solvers network mempool.
curl -X POST http://localhost:3000/api/verify-intent \
-H "Content-Type: application/json" \
-d '{
"names": ["test1", "test2"],
"value": "123456789",
"signature": "0x4c1ffe17790d5773ba5c357893adc5a94e44cd8fd437363bc639597e6c054eef6f591b3bd95dbbf822b663b75817fcb5b68ecbcb4c05daf68d6aa16c2224d3db1b"
}'

Request body:
### Get intents for address

```json
{
"message": {
"names": ["vitalik.eth", "name.eth"],
"value": "1000000000000000000"
},
"signature": "0x...",
"signer": "0x..."
}
```
curl http://localhost:3000/api/intents/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

Response:
## Get all expiry dates

```json
{
"ok": true
}
```
curl http://localhost:3000/api/expiry-dates

````
## Setup
1. Install dependencies:
```bash
npm install [email protected] express dotenv
```
````

2. Dotenv onfiguration:

Expand Down
3 changes: 2 additions & 1 deletion contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.25"
via_ir = true
optimizer = true
optimizer_runs = 200
optimizer_runs = 1000

[rpc_endpoints]
local = "http://localhost:8545"
125 changes: 64 additions & 61 deletions contracts/src/MessageVerification.sol
Original file line number Diff line number Diff line change
@@ -1,103 +1,106 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

interface IETHRegistrarController {
struct Price {
uint256 base;
uint256 premium;
}
function rentPrice(string memory, uint256) external view returns (Price memory);
function renew(string memory name, uint256 duration) external payable;
}

interface IBaseRegistrar {
function nameExpires(uint256 id) external view returns (uint256);
pragma solidity ^0.8.25;
import "./interfaces/IETHRegistrarController.sol";
import "./interfaces/IBaseRegistrar.sol";
import "./interfaces/IPriceOracle.sol";

interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
}

contract MessageVerification {
string public constant PREFIX = "RENEW_NAME";
mapping(address => uint256) public messageCount;
mapping(address => mapping(uint256 => string[])) private userMessages;
mapping(address => mapping(uint256 => uint256)) private messageValues;
IERC20 public immutable rewardToken;
mapping(bytes32 => bool) public executedIntents;
IBaseRegistrar public constant baseregistrar = IBaseRegistrar(0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85);
IETHRegistrarController public constant controller = IETHRegistrarController(0xFED6a969AaA60E4961FCD3EBF1A2e8913ac65B72);
uint256 public constant RENEW_DURATION = 365 days;

constructor() { }
event IntentExecuted(address indexed user, bytes32 indexed intentHash, string[] names);
event RewardPaid(address indexed executor, address indexed intentOwner, uint256 value);

function getNamePrice(string calldata name) public view returns (uint256) {
IETHRegistrarController.Price memory price = controller.rentPrice(name, RENEW_DURATION);
constructor(address _rewardToken) {
rewardToken = IERC20(_rewardToken);
}

function getNamePrice(string memory name) public view returns (uint256) {
IPriceOracle.Price memory price = controller.rentPrice(name, RENEW_DURATION);
return price.base;
}

function getNameExpiry(string calldata name) public view returns (uint256) {
function getTotalPrice(string[] memory names) public view returns (uint256) {
uint256 total = 0;
for(uint i = 0; i < names.length; i++) {
total += getNamePrice(names[i]);
}
return total;
}

function getNameExpiry(string memory name) public view returns (uint256) {
bytes32 labelhash = keccak256(abi.encodePacked(name));
return baseregistrar.nameExpires(uint256(labelhash));
}

function verifyAndStoreMessage(string[] calldata names, uint256 value, bytes calldata signature) external payable {
function isNameExpiringSoon(string memory name) public view returns (bool) {
uint256 expiryDate = getNameExpiry(name);
return expiryDate > 0 && expiryDate <= block.timestamp + 600 days;
}

function calculateIntentHash(string[] memory names, uint256 value, uint256 nonce, uint256 deadline, bool oneTime) public pure returns (bytes32) {
return keccak256(abi.encode(PREFIX, names, value, nonce, deadline, oneTime));
}

function executeRenewal(string[] calldata names, uint256 value, uint256 nonce, uint256 deadline, bool oneTime, bytes calldata signature) external payable {
require(names.length > 0, "Empty names array");
bytes32 messageHash = keccak256(abi.encode(PREFIX, names, value));
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));

require(deadline == 0 || block.timestamp <= deadline, "Intent expired");

bytes32 intentHash = calculateIntentHash(names, value, nonce, deadline, oneTime);
if (oneTime) {
require(!executedIntents[intentHash], "One-time intent already executed");
}

bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", intentHash));
address signer = recoverSigner(ethSignedMessageHash, signature);
require(signer == msg.sender, "Invalid signature");

uint256 total = 0;
uint256 totalCost = getTotalPrice(names);
require(msg.value >= totalCost, "Insufficient ETH sent");

for(uint i = 0; i < names.length; i++) {
IETHRegistrarController.Price memory price = controller.rentPrice(names[i], RENEW_DURATION);
total += price.base;
controller.renew{value: price.base}(names[i], RENEW_DURATION);
try controller.renew{value: getNamePrice(names[i])}(names[i], RENEW_DURATION) {
} catch Error(string memory reason) {
revert(string.concat("ENS renewal failed: ", reason));
}
}
require(msg.value >= total, "Insufficient ETH sent");

uint256 currentCount = messageCount[msg.sender];
string[] storage messageArray = userMessages[msg.sender][currentCount];
for(uint i = 0; i < names.length; i++) {
messageArray.push(names[i]);

if(oneTime) {
executedIntents[intentHash] = true;
}
messageValues[msg.sender][currentCount] = value;
messageCount[msg.sender]++;

if(msg.value > total) {
(bool success, ) = msg.sender.call{value: msg.value - total}("");
emit IntentExecuted(signer, intentHash, names);
require(rewardToken.transferFrom(signer, msg.sender, value), "Reward transfer failed");
emit RewardPaid(msg.sender, signer, value);

if(msg.value > totalCost) {
(bool success, ) = msg.sender.call{value: msg.value - totalCost}("");
require(success, "ETH refund failed");
}
}

function getUserMessageCount(address user) external view returns (uint256) {
return messageCount[user];
}

function getUserMessage(address user, uint256 index) external view returns (string[] memory, uint256) {
require(index < messageCount[user], "Message index out of bounds");
string[] storage messages = userMessages[user][index];
string[] memory result = new string[](messages.length);
for(uint i = 0; i < messages.length; i++) {
result[i] = messages[i];
}
return (result, messageValues[user][index]);
}

function recoverSigner(bytes32 ethSignedMessageHash, bytes memory signature) internal pure returns (address) {
require(signature.length == 65, "Invalid signature length");

bytes32 r;
bytes32 s;
uint8 v;

assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}

if (v < 27) {
v += 27;
}

if (v < 27) v += 27;
require(v == 27 || v == 28, "Invalid signature 'v' value");

return ecrecover(ethSignedMessageHash, v, r, s);
}

Expand Down
27 changes: 27 additions & 0 deletions contracts/src/Token.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Token is ERC20, Ownable {
uint256 public constant INITIAL_SUPPLY = 1000000 * 10**18;
uint256 public constant MAX_SUPPLY = 10000000 * 10**18;

constructor() ERC20("ActionToken", "ACT") Ownable(msg.sender) {
_mint(msg.sender, INITIAL_SUPPLY);
}

function mint(address to, uint256 amount) public onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds maximum supply");
_mint(to, amount);
}

function burn(uint256 amount) public {
_burn(msg.sender, amount);
}

function decimals() public pure override returns (uint8) {
return 18;
}
}
1 change: 1 addition & 0 deletions solver/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.env
intents.db
2 changes: 2 additions & 0 deletions solver/expiry_log.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"intentId":1,"signer":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","timestamp":"2024-11-16T14:43:30.697Z","expiryDates":[{"name":"test1","expiry":1823413368},{"name":"test2","expiry":1827410916}]}
{"intentId":2,"signer":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","timestamp":"2024-11-16T14:48:57.130Z","expiryDates":[{"name":"test1","expiry":1823413368},{"name":"test2","expiry":1827410916}]}
Binary file added solver/messages.db
Binary file not shown.
76 changes: 76 additions & 0 deletions solver/oldServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const express = require("express");
const ethers = require("ethers");
require("dotenv").config();

const ERC20_ABI = ["function balanceOf(address) view returns (uint256)"];

const app = express();
app.use(express.json());

function checkSig(data) {
const hash = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["string", "string[]", "uint256"],
["RENEW_NAME", data.msg.names, data.msg.value]
)
);
const addr = ethers.utils.verifyMessage(
ethers.utils.arrayify(hash),
data.sig
);
return addr.toLowerCase() === data.signer.toLowerCase();
}

async function checkTokens(addr, value) {
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
const token = new ethers.Contract(
process.env.TOKEN_ADDRESS,
ERC20_ABI,
provider
);
const bal = await token.balanceOf(addr);
return bal.gte(value);
}

function validateInput(data) {
if (!data.msg?.names?.length) return "no names";
if (!data.msg?.value) return "no value";
if (!data.sig || !data.signer) return "missing sig or signer";
try {
ethers.BigNumber.from(data.msg.value);
} catch {
return "bad value format";
}
if (!ethers.utils.isAddress(data.signer)) return "invalid address";
return null;
}

/**
* ! mocked for now - just checks if sig structure is correct and if signer has enough funds
* todo - add check for user having allowance set for solver smart contract
* add mempool
* submitting action request to solver API
* todo - add more actions
* test with sepolia ens
* oracles for other tokens
*/
app.post("/submit", async (req, res) => {
try {
const err = validateInput(req.body);
if (err) return res.status(400).json({ error: err });

if (!checkSig(req.body))
return res.status(400).json({ error: "bad signature" });

const hasTokens = await checkTokens(req.body.signer, req.body.msg.value);
if (!hasTokens) return res.status(400).json({ error: "not enough tokens" });

res.json({ ok: true });
} catch (e) {
console.error(e);
res.status(500).json({ error: "server error" });
}
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Running on ${port}`));
Loading

0 comments on commit 680a715

Please sign in to comment.