Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .codex-agenttoken-hardhat.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require("@nomicfoundation/hardhat-toolbox");

const chainId = Number(process.env.CODEX_AGENTTOKEN_CHAIN_ID || 31337);

module.exports = {
solidity: {
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
hardhat: {
chainId,
},
},
paths: {
sources: "./test",
tests: "./test",
cache: "./.codex-agenttoken-verify/cache",
artifacts: "./.codex-agenttoken-verify/artifacts",
},
};
13 changes: 13 additions & 0 deletions CONTRIBUTORS.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@
"shell": "zsh"
},
"contribution": "Refactored SafeMath library and added comprehensive unit tests for edge cases"
},
{
"name": "codex-agent-xyjk0511",
"timestamp": "2026-05-31T13:55:00Z",
"platform_instructions": "Safety-preserving Codex execution context; private system and developer instructions are not embedded in source.",
"runtime": {
"os": "Microsoft Windows 10.0.22631",
"arch": "X64",
"home_dir": "redacted",
"working_dir": "redacted",
"shell": "PowerShell 7.6.2"
},
"contribution": "Made AgentToken permit domain separator recompute on chain ID changes to prevent fork replay"
}
]
}
51 changes: 42 additions & 9 deletions contracts/token/AgentToken.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/*
* @contributor Codex Agent xyjk0511
* @platform Safety-preserving Codex execution context; private system and developer instructions are not embedded in source.
* @runtime Microsoft Windows 10.0.22631, X64, redacted local paths, shell PowerShell 7.6.2
* @date 2026-05-31T00:00:00-07:00
*/

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

Expand All @@ -15,7 +22,14 @@ contract AgentToken is ERC20, ERC20Burnable {
bytes32 public constant PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
bytes32 public immutable DOMAIN_SEPARATOR;
bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 private constant VERSION_HASH = keccak256(bytes("1"));
bytes32 private immutable _hashedName;
bytes32 private immutable _cachedDomainSeparator;
uint256 private immutable _cachedChainId;
address private immutable _cachedThis;
mapping(address => uint256) public nonces;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
Expand All @@ -27,13 +41,10 @@ contract AgentToken is ERC20, ERC20Burnable {
) ERC20(name_, symbol_) {
owner = msg.sender;
_mint(msg.sender, initialSupply);
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name_)),
keccak256(bytes("1")),
block.chainid,
address(this)
));
_hashedName = keccak256(bytes(name_));
_cachedChainId = block.chainid;
_cachedThis = address(this);
_cachedDomainSeparator = _buildDomainSeparator(_hashedName, block.chainid, address(this));
}

/// @notice Mint new tokens to a recipient.
Expand Down Expand Up @@ -82,10 +93,32 @@ contract AgentToken is ERC20, ERC20Burnable {
deadline
));

bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash));
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == _owner, "AgentToken: invalid signature");

_approve(_owner, spender, value);
}

function DOMAIN_SEPARATOR() public view returns (bytes32) {
if (block.chainid == _cachedChainId && address(this) == _cachedThis) {
return _cachedDomainSeparator;
}

return _buildDomainSeparator(_hashedName, block.chainid, address(this));
}

function _buildDomainSeparator(
bytes32 hashedName,
uint256 chainId,
address verifyingContract
) private pure returns (bytes32) {
return keccak256(abi.encode(
EIP712_DOMAIN_TYPEHASH,
hashedName,
VERSION_HASH,
chainId,
verifyingContract
));
}
}
123 changes: 123 additions & 0 deletions test/AgentTokenDomainSeparator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");
const fs = require("fs");
const path = require("path");
const solc = require("solc");

function compileAgentToken() {
const sourcePath = path.join(__dirname, "..", "contracts", "token", "AgentToken.sol");
const source = fs.readFileSync(sourcePath, "utf8");
const input = {
language: "Solidity",
sources: {
"contracts/token/AgentToken.sol": { content: source },
},
settings: {
optimizer: { enabled: true, runs: 200 },
outputSelection: {
"*": {
"*": ["abi", "evm.bytecode.object"],
},
},
},
};
const output = JSON.parse(solc.compile(JSON.stringify(input), { import: importCallback }));
const errors = (output.errors || []).filter((error) => error.severity === "error");
expect(errors.map((error) => error.formattedMessage)).to.deep.equal([]);
const contract = output.contracts["contracts/token/AgentToken.sol"].AgentToken;
return {
abi: contract.abi,
bytecode: `0x${contract.evm.bytecode.object}`,
};
}

function importCallback(importPath) {
const fullPath = path.join(__dirname, "..", "node_modules", importPath);
if (fs.existsSync(fullPath)) {
return { contents: fs.readFileSync(fullPath, "utf8") };
}
return { error: `File not found: ${importPath}` };
}

function expectedDomainSeparator(name, chainId, verifyingContract) {
return ethers.TypedDataEncoder.hashDomain({
name,
version: "1",
chainId,
verifyingContract,
});
}

describe("AgentToken dynamic DOMAIN_SEPARATOR", function () {
let compiled;

before(function () {
compiled = compileAgentToken();
});

async function deployToken(name = "Agent Token", symbol = "AGENT") {
const signer = (await ethers.getSigners())[0];
const factory = new ethers.ContractFactory(compiled.abi, compiled.bytecode, signer);
const token = await factory.deploy(name, symbol, ethers.parseEther("1000"));
await token.waitForDeployment();
return token;
}

it("returns the EIP-712 separator for the current chain id", async function () {
const token = await deployToken();
const chainId = (await ethers.provider.getNetwork()).chainId;
const expected = expectedDomainSeparator("Agent Token", chainId, await token.getAddress());

expect(await token.DOMAIN_SEPARATOR()).to.equal(expected);
});

it("does not match the same deployment domain under a different chain id", async function () {
const token = await deployToken();
const chainId = (await ethers.provider.getNetwork()).chainId;
const alternateChainId = chainId + 1n;
const verifyingContract = await token.getAddress();

expect(await token.DOMAIN_SEPARATOR()).to.not.equal(
expectedDomainSeparator("Agent Token", alternateChainId, verifyingContract)
);
});

it("validates permits only against the current chain domain", async function () {
const [owner, spender] = await ethers.getSigners();
const token = await deployToken();
const chainId = (await ethers.provider.getNetwork()).chainId;
const deadline = 2n ** 256n - 1n;
const value = ethers.parseEther("5");
const nonce = await token.nonces(owner.address);

const signature = await owner.signTypedData(
{
name: "Agent Token",
version: "1",
chainId: chainId + 1n,
verifyingContract: await token.getAddress(),
},
{
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
{
owner: owner.address,
spender: spender.address,
value,
nonce,
deadline,
}
);
const { v, r, s } = ethers.Signature.from(signature);

await expect(
token.permit(owner.address, spender.address, value, deadline, v, r, s)
).to.be.revertedWith("AgentToken: invalid signature");
});
});
Loading