Skip to content

Commit 23c8d41

Browse files
committed
feat: integrate OpenZeppelin SafeERC20, tighten pause semantics, add tests
- Switch USDC handling to OpenZeppelin IERC20 + SafeERC20 - Enforce pause across policy and recipient management - Add basic policy sanity checks (per-tx > 0, daily >= per-tx) - Clean up agent registry access and unused variables - Add Foundry test suite covering happy path, pause, replay, and revocation
1 parent 3869657 commit 23c8d41

File tree

8 files changed

+155
-12
lines changed

8 files changed

+155
-12
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "lib/forge-std"]
22
path = lib/forge-std
33
url = https://github.com/foundry-rs/forge-std
4+
[submodule "lib/openzeppelin-contracts"]
5+
path = lib/openzeppelin-contracts
6+
url = https://github.com/OpenZeppelin/openzeppelin-contracts

foundry.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"name": "v1.14.0",
55
"rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6"
66
}
7+
},
8+
"lib/openzeppelin-contracts": {
9+
"tag": {
10+
"name": "v5.5.0",
11+
"rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565"
12+
}
713
}
814
}

foundry.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ out = "out"
44
libs = ["lib"]
55
solc_version = "0.8.26"
66

7+
remappings = [
8+
"@openzeppelin/contracts=lib/openzeppelin-contracts/contracts"
9+
]
710
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

lib/openzeppelin-contracts

Submodule openzeppelin-contracts added at fcbae53

src/AgentRegistry.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ contract AgentRegistry {
104104
return (a.owner, a.active);
105105
}
106106

107-
108107
//////////////////////
109108
/// External view Functions
110109
//////////////////////

src/TreasuryWithPolicy.sol

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.26;
33

4-
import "./AgentRegistry.sol";
5-
6-
interface IERC20 {
7-
function transfer(address to, uint256 amount) external returns (bool);
8-
}
4+
import {AgentRegistry} from "./AgentRegistry.sol";
5+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
97

108
contract TreasuryWithPolicy {
9+
using SafeERC20 for IERC20;
10+
1111
error NotOwner();
1212
error AgentNotAuthorized();
1313
error RecipientNotAllowed();
@@ -30,7 +30,7 @@ contract TreasuryWithPolicy {
3030
uint256 cooldown;
3131
}
3232

33-
IERC20 public immutable usdc;
33+
IERC20 public immutable USDC;
3434
address public owner;
3535
address public agentRegistry;
3636

@@ -58,7 +58,7 @@ contract TreasuryWithPolicy {
5858
constructor(address _usdc, address _agentRegistry) {
5959
require(_usdc != address(0) && _agentRegistry != address(0), "zero address");
6060

61-
usdc = IERC20(_usdc);
61+
USDC = IERC20(_usdc);
6262
agentRegistry = _agentRegistry;
6363
owner = msg.sender;
6464
}
@@ -83,7 +83,7 @@ contract TreasuryWithPolicy {
8383
if (paused) revert TreasuryPaused();
8484

8585
// Identity check via registry
86-
(address agentOwner, bool active) = AgentRegistry(agentRegistry).getAgent(intent.agent);
86+
(, bool active) = AgentRegistry(agentRegistry).getAgent(intent.agent);
8787

8888
if (!active) revert AgentNotAuthorized();
8989

@@ -113,24 +113,32 @@ contract TreasuryWithPolicy {
113113
lastPaymentTime[intent.agent] = block.timestamp;
114114

115115
// ---- interaction ----
116-
bool ok = usdc.transfer(intent.recipient, intent.amount);
117-
require(ok, "transfer failed");
116+
USDC.safeTransfer(intent.recipient, intent.amount);
118117

119118
emit PaymentExecuted(intent.agent, intent.recipient, intent.amount, intent.nonce);
120119
}
121120

122121
function updatePolicy(uint256 perTxLimit, uint256 dailyLimit, uint256 cooldown) external onlyOwner {
122+
if (paused) revert TreasuryPaused();
123+
124+
require(perTxLimit > 0, "perTxLimit=0");
125+
require(dailyLimit >= perTxLimit, "daily < perTx");
126+
123127
policy = Policy({perTxLimit: perTxLimit, dailyLimit: dailyLimit, cooldown: cooldown});
124128

125129
emit PolicyUpdated(perTxLimit, dailyLimit, cooldown);
126130
}
127131

128132
function allowRecipient(address recipient) external onlyOwner {
133+
if (paused) revert TreasuryPaused();
134+
129135
allowedRecipients[recipient] = true;
130136
emit RecipientAllowed(recipient);
131137
}
132138

133139
function removeRecipient(address recipient) external onlyOwner {
140+
if (paused) revert TreasuryPaused();
141+
134142
allowedRecipients[recipient] = false;
135143
emit RecipientRemoved(recipient);
136144
}
@@ -149,7 +157,7 @@ contract TreasuryWithPolicy {
149157
/// Internal Functions
150158
//////////////////////
151159

152-
function _onlyOwner() internal {
160+
function _onlyOwner() internal view {
153161
if (msg.sender != owner) revert NotOwner();
154162
}
155163

test/TreasuryWithPolicy.t.sol

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import "forge-std/Test.sol";
5+
import {MockUSDC} from "./mocks/MockUSDC.sol";
6+
import {AgentRegistry} from "../src/AgentRegistry.sol";
7+
import {TreasuryWithPolicy} from "../src/TreasuryWithPolicy.sol";
8+
9+
contract TreasuryWithPolicyTest is Test {
10+
AgentRegistry registry;
11+
TreasuryWithPolicy treasury;
12+
13+
address owner = makeAddr("owner");
14+
address agent = makeAddr("agent");
15+
address recipient = makeAddr("recipient");
16+
17+
MockUSDC usdc;
18+
19+
function setUp() public {
20+
vm.startPrank(owner);
21+
22+
usdc = new MockUSDC();
23+
registry = new AgentRegistry();
24+
treasury = new TreasuryWithPolicy(address(usdc), address(registry));
25+
26+
registry.registerAgent(agent);
27+
28+
usdc.mint(address(treasury), 1_000_000e6);
29+
30+
treasury.updatePolicy(
31+
100e6, // perTxLimit
32+
500e6, // dailyLimit
33+
1 hours // cooldown
34+
);
35+
36+
treasury.allowRecipient(recipient);
37+
38+
vm.stopPrank();
39+
}
40+
41+
function testExecutePaymentHappyPath() public {
42+
TreasuryWithPolicy.PaymentIntent memory intent =
43+
TreasuryWithPolicy.PaymentIntent({
44+
agent: agent,
45+
recipient: recipient,
46+
amount: 50e6,
47+
nonce: 1
48+
});
49+
50+
vm.prank(address(0xdead)); // relayer / bot
51+
treasury.executePayment(intent);
52+
53+
assertEq(
54+
MockUSDC(address(usdc)).balanceOf(recipient),
55+
50e6
56+
);
57+
}
58+
59+
function testRevokedAgentCannotExecute() public {
60+
vm.prank(owner);
61+
registry.revokeAgent(agent);
62+
63+
TreasuryWithPolicy.PaymentIntent memory intent =
64+
TreasuryWithPolicy.PaymentIntent({
65+
agent: agent,
66+
recipient: recipient,
67+
amount: 10e6,
68+
nonce: 2
69+
});
70+
71+
vm.expectRevert(TreasuryWithPolicy.AgentNotAuthorized.selector);
72+
treasury.executePayment(intent);
73+
}
74+
75+
function testNonceReplayBlocked() public {
76+
TreasuryWithPolicy.PaymentIntent memory intent =
77+
TreasuryWithPolicy.PaymentIntent({
78+
agent: agent,
79+
recipient: recipient,
80+
amount: 10e6,
81+
nonce: 3
82+
});
83+
84+
treasury.executePayment(intent);
85+
86+
vm.expectRevert(TreasuryWithPolicy.NonceAlreadyUsed.selector);
87+
treasury.executePayment(intent);
88+
}
89+
90+
function testPauseBlocksExecution() public {
91+
vm.prank(owner);
92+
treasury.pause();
93+
94+
TreasuryWithPolicy.PaymentIntent memory intent =
95+
TreasuryWithPolicy.PaymentIntent({
96+
agent: agent,
97+
recipient: recipient,
98+
amount: 10e6,
99+
nonce: 4
100+
});
101+
102+
vm.expectRevert(TreasuryWithPolicy.TreasuryPaused.selector);
103+
treasury.executePayment(intent);
104+
}
105+
106+
}

test/mocks/MockUSDC.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
contract MockUSDC {
5+
mapping(address => uint256) public balanceOf;
6+
7+
function mint(address to, uint256 amount) external {
8+
balanceOf[to] += amount;
9+
}
10+
11+
function transfer(address to, uint256 amount) external returns (bool) {
12+
require(balanceOf[msg.sender] >= amount, "insufficient");
13+
balanceOf[msg.sender] -= amount;
14+
balanceOf[to] += amount;
15+
return true;
16+
}
17+
}

0 commit comments

Comments
 (0)